odant-work 1.0.15 → 1.0.17

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 (84) hide show
  1. package/#system/package-manifest.json +57 -49
  2. package/$server/$folder/handlers/methods/create/$handler/class.js +83 -2
  3. package/$server/$folder/handlers/pages/form/$handler/form.js +2 -1
  4. package/$server/$folder/lib/icon/icon.js +14 -0
  5. package/$server/$folder/lib/node/node.js +51 -1
  6. package/$server/$folder/lib/tree/tree.js +5 -1
  7. package/.secretsignore +4 -0
  8. package/MODELS/$ai/$folder/$class/$ai/class.js +58 -3
  9. package/MODELS/$ai/$folder/$class/$ai/handlers/pages/form/trainer/$handler/class.js +6 -4
  10. package/MODELS/$ai/$folder/$class/$ai/handlers/pages/form/trainer/$handler/trainer.js +344 -3
  11. package/MODELS/$ai/$folder/$class/$provider/class.js +1 -1
  12. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/class.js +13 -0
  13. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/provider.js +479 -0
  14. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/class.js +3 -3
  15. package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/trainer.js +530 -920
  16. package/MODELS/GigaChat/$provider/$folder/$class/$ai/class.js +10 -20
  17. package/MODELS/GigaChat/$provider/class.js +10 -7
  18. package/MODELS/GigaChat/GigaChat 2 Lite/$ai/class.js +15 -0
  19. package/MODELS/GigaChat/GigaChat 2 Max/$ai/class.js +16 -0
  20. package/MODELS/GigaChat/GigaChat 2 Pro/$ai/class.js +15 -0
  21. package/MODELS/GigaChat/GigaChat Embeddings/$ai/class.js +13 -0
  22. package/MODELS/Local/$provider/$folder/$class/$ai/class.js +3 -3
  23. package/MODELS/Local/$provider/class.js +7 -3
  24. package/MODELS/MTS-AI/$provider/$folder/$class/$ai/class.js +8 -0
  25. package/MODELS/MTS-AI/$provider/class.js +8 -0
  26. package/MODELS/MTS-AI/Cotype Lite/$ai/class.js +15 -0
  27. package/MODELS/MTS-AI/Cotype Pro 2/$ai/class.js +15 -0
  28. package/MODELS/MTS-AI/Cotype Vision/$ai/class.js +14 -0
  29. package/MODELS/T-Bank/$provider/$folder/$class/$ai/class.js +8 -0
  30. package/MODELS/T-Bank/$provider/class.js +8 -0
  31. package/MODELS/T-Bank/T-Lite 2.0/$ai/class.js +15 -0
  32. package/MODELS/T-Bank/T-Pro 2.0/$ai/class.js +16 -0
  33. package/MODELS/T-Bank/T-VoiceKit ASR/$ai/class.js +13 -0
  34. package/MODELS/T-Bank/T-VoiceKit TTS/$ai/class.js +13 -0
  35. package/MODELS/Yandex/$provider/$folder/$class/$ai/class.js +9 -0
  36. package/MODELS/Yandex/$provider/class.js +9 -0
  37. package/MODELS/Yandex/Alice AI (YandexGPT 5 Pro)/$ai/class.js +15 -0
  38. package/MODELS/Yandex/Yandex Embeddings/$ai/class.js +13 -0
  39. package/MODELS/Yandex/Yandex SpeechKit ASR/$ai/class.js +13 -0
  40. package/MODELS/Yandex/Yandex SpeechKit TTS/$ai/class.js +13 -0
  41. package/MODELS/Yandex/YandexART/$ai/class.js +13 -0
  42. package/MODELS/Yandex/YandexGPT 5 Lite/$ai/class.js +15 -0
  43. package/MODELS/Yandex/YandexGPT Reasoning/$ai/class.js +22 -0
  44. package/oda/tools/icons/lib/svg/ai.svg +20 -0
  45. package/package.json +1 -1
  46. package/scripts/check-ignored-code.mjs +3 -0
  47. package/scripts/check-secrets.mjs +1 -1
  48. package/sources/data/progress.md +24 -0
  49. package/sources/host/watch.js +13 -2
  50. package/sources/modules/agent/index.js +23 -2
  51. package/sources/modules/trainer/halogen.js +128 -29
  52. package/sources/modules/trainer/huggingface.js +11 -5
  53. package/sources/modules/trainer/index.js +176 -3
  54. package/sources/modules/trainer/local-engine.js +67 -1
  55. package/sources/modules/trainer/providers.js +1213 -0
  56. package/sources/modules/trainer/runner.js +47 -1
  57. package/sources/server/class.js +55 -3
  58. package/MODELS/GigaChat/GigaChat Light/$ai/class.js +0 -15
  59. package/MODELS/GigaChat/GigaChat Pro/$ai/class.js +0 -15
  60. package/MODELS/Halogen/$provider/$folder/$class/$ai/class.js +0 -11
  61. package/MODELS/Halogen/$provider/class.js +0 -16
  62. package/MODELS/Halogen/$provider/readme.md +0 -28
  63. package/MODELS/Halogen/Decider-NPU/$ai/class.js +0 -22
  64. package/MODELS/Halogen/Flux2-Klein-NPU/$ai/class.js +0 -21
  65. package/MODELS/Halogen/Qwen3-ASR-NPU/$ai/class.js +0 -21
  66. package/MODELS/Halogen/Qwen3-Embedding-NPU/$ai/class.js +0 -22
  67. package/MODELS/Halogen/Qwen3-Reranker-NPU/$ai/class.js +0 -21
  68. package/MODELS/Halogen/Qwen3-TTS-NPU/$ai/class.js +0 -26
  69. package/MODELS/Halogen/Qwen3.5-2B-NPU/$ai/class.js +0 -23
  70. package/MODELS/Halogen/Qwen3.8-Flash-Next/$ai/class.js +0 -31
  71. package/MODELS/Local/Flux-Image/$ai/class.js +0 -20
  72. package/MODELS/Local/Qwen3-0.6B/$ai/class.js +0 -26
  73. package/MODELS/Local/Qwen3-14B/$ai/class.js +0 -27
  74. package/MODELS/Local/Qwen3-30B-A3B/$ai/class.js +0 -30
  75. package/MODELS/Local/Qwen3-Embedding/$ai/class.js +0 -21
  76. package/MODELS/Local/Qwen3-Voice/$ai/class.js +0 -26
  77. package/MODELS/odant/$provider/$folder/$class/$ai/class.js +0 -11
  78. package/MODELS/odant/$provider/class.js +0 -75
  79. package/MODELS/odant/$provider/readme.md +0 -21
  80. package/MODELS/odant/Qwen3-ASR/$ai/class.js +0 -15
  81. package/MODELS/odant/Qwen3-TTS/$ai/class.js +0 -22
  82. package/MODELS/odant/Qwen3.8 27b/$ai/class.js +0 -11
  83. package/MODELS/odant/Z-Image-Turbo/$ai/class.js +0 -8
  84. package/MODELS/odant/Z-Image-Turbo/$ai/readme.md +0 -24
@@ -1,35 +1,9 @@
1
1
  /**
2
- * form/trainer — мультимодальная витрина локальных моделей Hugging Face Hub (LLM, VLM/OCR, Image, Audio ASR/TTS/Music, Video, RAG),
3
- * сборщик датасетов WORK и контур дообучения ИИ (LoRA / SFT / GRPO) с адаптацией под реальное железо хоста.
2
+ * form/trainer («Модели ИИ» на /MODELS/Local) — чистый интерфейс управления локальными моделями ИИ:
3
+ * - встроенный в WORK сервер локального ИИ (автозапуск по запросу, постоянный режим на Halo ≥ 128 ГБ, выгрузка при простое на обычных ПК);
4
+ * - вкладка «Модели»: установленные модели (Вкл/Выкл, Удалить с диска) + установка моделей в 1 клик (включая комплект Halo);
5
+ * - вкладка «Дообучение»: двухшаговый мастер (1. Собрать данные организации → 2. Дообучить модель LoRA).
4
6
  */
5
-
6
- const MODALITY_TABS = [
7
- { id: 'all', label: 'Все типы', icon: 'icons:apps' },
8
- { id: 'llm', label: 'Текст и Код', icon: 'icons:description' },
9
- { id: 'vlm', label: 'Зрение / OCR', icon: 'image:remove-red-eye' },
10
- { id: 'image', label: 'Картинки', icon: 'image:brush' },
11
- { id: 'audio', label: 'Звук и Речь', icon: 'av:mic' },
12
- { id: 'video', label: 'Видео', icon: 'av:videocam' },
13
- { id: 'embed', label: 'Поиск (RAG)', icon: 'icons:search' },
14
- ];
15
-
16
- const CAPABILITY_OPTIONS = [
17
- { id: '', label: 'Все умения' },
18
- { id: 'chat', label: 'Диалог и консультации' },
19
- { id: 'tools', label: 'Вызов инструментов WORK' },
20
- { id: 'thinking', label: 'Глубокие рассуждения (CoT)' },
21
- { id: 'coding', label: 'Генерация кода и схем' },
22
- { id: 'vision', label: 'Зрение и чтение документов (OCR)' },
23
- { id: 'image', label: 'Генерация изображений' },
24
- { id: 'stt', label: 'Распознавание речи (ASR / STT)' },
25
- { id: 'tts', label: 'Синтез речи и голоса (TTS)' },
26
- { id: 'music', label: 'Генерация музыки и звуков' },
27
- { id: 'video', label: 'Генерация видеороликов' },
28
- { id: 'embeddings', label: 'Векторные эмбеддинги (RAG)' },
29
- { id: 'rerank', label: 'Ранжирование поиска (Rerank)' },
30
- { id: 'lora', label: 'Поддержка дообучения LoRA' },
31
- ];
32
-
33
7
  export default {
34
8
  imports: 'oda//button, oda//icon',
35
9
  template: /*html*/`
@@ -37,860 +11,594 @@ export default {
37
11
  :host {
38
12
  @apply --vertical;
39
13
  @apply --flex;
40
- overflow: hidden;
14
+ overflow-y: auto;
41
15
  background: var(--content-background, #ffffff);
42
16
  color: var(--content-color, #1e293b);
43
17
  font-size: 13px;
18
+ padding: 16px 20px;
19
+ gap: 14px;
20
+ box-sizing: border-box;
44
21
  }
45
- /* Верхняя плашка оборудования */
46
- .hw-banner {
22
+ .header-card {
47
23
  @apply --horizontal;
48
- flex-shrink: 0;
49
24
  align-items: center;
50
25
  justify-content: space-between;
51
- gap: 10px;
52
- padding: 8px 14px;
53
- background: light-dark(linear-gradient(90deg, #f3f0ff 0%, #f8fafc 100%), linear-gradient(90deg, #271745 0%, #1a1b22 100%));
54
- border-bottom: 1px solid light-dark(#d8b4fe, #4c1d95);
26
+ gap: 12px;
27
+ padding: 12px 16px;
28
+ border-radius: 8px;
29
+ border: 1px solid var(--border-color, #cbd5e1);
30
+ background: var(--subtle-background, #f8fafc);
55
31
  flex-wrap: wrap;
56
32
  }
57
- .hw-left {
58
- @apply --vertical;
59
- gap: 3px;
33
+ .header-left {
34
+ @apply --horizontal;
35
+ align-items: center;
36
+ gap: 12px;
60
37
  min-width: 0;
61
38
  }
62
- .hw-title {
39
+ .title {
40
+ font-size: 16px;
63
41
  font-weight: 700;
64
- font-size: 14px;
65
- color: light-dark(#4c1d95, #e9d5ff);
66
42
  }
67
- .hw-specs {
68
- @apply --horizontal;
69
- gap: 6px;
70
- align-items: center;
71
- flex-wrap: wrap;
43
+ .hw-line {
72
44
  font-size: 12px;
73
- color: light-dark(#334155, #cbd5e1);
45
+ opacity: 0.8;
46
+ margin-top: 2px;
74
47
  }
75
- .hw-chip {
76
- display: inline-flex;
77
- align-items: center;
78
- gap: 4px;
79
- padding: 1px 8px;
80
- border-radius: 4px;
81
- background: light-dark(rgba(255,255,255,0.85), rgba(255,255,255,0.07));
82
- border: 1px solid light-dark(#cbd5e1, #3f3f46);
83
- white-space: nowrap;
84
- }
85
- .hw-badges {
48
+ .toolbar {
86
49
  @apply --horizontal;
87
- gap: 6px;
50
+ gap: 8px;
88
51
  align-items: center;
89
52
  flex-wrap: wrap;
90
53
  }
91
- /* Универсальные неразрывные бейджи */
92
54
  .badge {
93
55
  display: inline-flex;
94
56
  align-items: center;
95
- gap: 4px;
96
- padding: 2px 8px;
57
+ gap: 6px;
58
+ padding: 3px 10px;
97
59
  border-radius: 999px;
98
- font-size: 11.5px;
60
+ font-size: 12px;
99
61
  font-weight: 600;
100
- line-height: 1.35;
101
62
  white-space: nowrap;
102
- flex-shrink: 0;
103
- background: light-dark(#f1f5f9, #2d2f39);
104
- color: light-dark(#334155, #e2e8f0);
105
- border: 1px solid light-dark(#cbd5e1, #475569);
106
63
  }
107
- .badge.purple {
108
- background: light-dark(#f3e8ff, #3b0764);
109
- border-color: light-dark(#c084fc, #7e22ce);
110
- color: light-dark(#6b21a8, #e9d5ff);
111
- }
112
- .badge.optimal {
64
+ .badge.active {
113
65
  background: light-dark(#dcfce7, #14532d);
114
- border-color: light-dark(#86efac, #16a34a);
115
66
  color: light-dark(#166534, #bbf7d0);
67
+ border: 1px solid light-dark(#86efac, #16a34a);
116
68
  }
117
- .badge.warn {
118
- background: light-dark(#fef9c3, #422006);
119
- border-color: light-dark(#fde047, #ca8a04);
120
- color: light-dark(#854d0e, #fef08a);
121
- }
122
- .badge.heavy {
123
- background: light-dark(#fee2e2, #450a0a);
124
- border-color: light-dark(#fca5a5, #dc2626);
125
- color: light-dark(#991b1b, #fecaca);
126
- }
127
- .badge.mod {
128
- background: light-dark(#e0e7ff, #1e1b4b);
129
- border-color: light-dark(#a5b4fc, #4338ca);
130
- color: light-dark(#3730a3, #c7d2fe);
69
+ .badge.neutral {
70
+ background: light-dark(#f1f5f9, #27272a);
71
+ color: light-dark(#475569, #a1a1aa);
72
+ border: 1px solid light-dark(#cbd5e1, #3f3f46);
131
73
  }
132
- .cap-pill {
133
- display: inline-flex;
134
- align-items: center;
135
- padding: 1px 7px;
136
- border-radius: 4px;
137
- font-size: 11px;
138
- font-weight: 600;
139
- white-space: nowrap;
140
- background: light-dark(#f8fafc, #1e293b);
141
- color: light-dark(#0f172a, #cbd5e1);
142
- border: 1px solid light-dark(#cbd5e1, #334155);
74
+ .dot {
75
+ width: 8px;
76
+ height: 8px;
77
+ border-radius: 50%;
78
+ display: inline-block;
79
+ flex-shrink: 0;
143
80
  }
81
+ .dot.on { background: #22c55e; }
82
+ .dot.warn { background: #f59e0b; }
83
+ .dot.off { background: #94a3b8; }
144
84
 
145
- /* Главные вкладки экрана */
85
+ /* Спокойные вкладки */
146
86
  .tabs-bar {
147
87
  @apply --horizontal;
148
- flex-shrink: 0;
149
88
  gap: 4px;
150
- padding: 5px 12px 0 12px;
151
- background: light-dark(#efeee4, #24252b);
152
- border-bottom: 1px solid light-dark(#b5b4ac, #3e4049);
89
+ border-bottom: 1px solid var(--border-color, #cbd5e1);
153
90
  }
154
91
  .tab-btn {
155
- padding: 6px 14px;
156
- font-size: 12.5px;
157
- font-weight: 600;
158
- border: 1px solid transparent;
159
- border-bottom: none;
160
- border-radius: 5px 5px 0 0;
92
+ appearance: none;
93
+ border: none;
94
+ border-bottom: 2px solid transparent;
161
95
  background: transparent;
162
- color: light-dark(#475569, #94a3b8);
96
+ color: inherit;
97
+ opacity: 0.7;
98
+ padding: 8px 16px;
99
+ font-size: 13px;
100
+ font-weight: 600;
163
101
  cursor: pointer;
164
- white-space: nowrap;
165
102
  }
166
103
  .tab-btn:hover {
167
- color: light-dark(#1e293b, #f1f5f9);
104
+ opacity: 1;
168
105
  }
169
106
  .tab-btn.tab-active {
170
- background: var(--content-background, #ffffff);
171
- border-color: light-dark(#b5b4ac, #3e4049);
172
- color: light-dark(#5b21b6, #c4b5fd);
173
- }
174
-
175
- .error {
176
- padding: 6px 12px;
177
- color: var(--error-color, #c53030);
178
- background: light-dark(#fff5f5, rgba(229, 62, 62, 0.15));
179
- border-bottom: 1px solid var(--error-color, #feb2b2);
180
- flex-shrink: 0;
107
+ opacity: 1;
108
+ color: var(--accent-color, #2563eb);
109
+ border-bottom-color: var(--accent-color, #2563eb);
181
110
  }
182
111
 
183
- /* Двухпанельная рабочая область */
184
- .workspace {
185
- @apply --horizontal;
186
- flex: 1;
187
- min-height: 0;
188
- overflow: hidden;
189
- }
190
- .panel-left {
191
- @apply --vertical;
192
- flex: 1.38;
193
- min-width: 0;
194
- min-height: 0;
195
- overflow: hidden;
196
- border-right: 1px solid light-dark(#c8c7be, #3e4049);
197
- }
198
- .sticky-header {
199
- @apply --vertical;
200
- flex-shrink: 0;
201
- gap: 8px;
202
- padding: 10px 12px;
203
- background: light-dark(#f8f8f4, #202127);
204
- border-bottom: 1px solid light-dark(#dcdbd2, #343640);
205
- }
206
- .cards-scroll {
207
- @apply --vertical;
208
- flex: 1;
209
- min-height: 0;
210
- overflow-y: auto;
211
- padding: 10px 12px;
212
- gap: 8px;
213
- }
214
- .panel-right {
112
+ .card {
215
113
  @apply --vertical;
216
- flex: 1;
217
- min-width: 320px;
218
- min-height: 0;
219
- overflow-y: auto;
220
- padding: 10px 12px;
221
114
  gap: 10px;
222
- background: light-dark(#fafaf7, #1b1c22);
223
- }
224
-
225
- /* Полоса выбора категории модели (Текст, Зрение, Картинки, Звук, Видео, RAG) */
226
- .modality-bar {
227
- @apply --horizontal;
228
- gap: 5px;
229
- flex-wrap: wrap;
230
- }
231
- .mod-btn {
232
- display: inline-flex;
233
- align-items: center;
234
- gap: 5px;
235
- padding: 4px 10px;
236
- font-size: 12px;
237
- font-weight: 600;
238
- border: 1px solid light-dark(#cbd5e1, #3f414d);
239
- border-radius: 6px;
240
- background: light-dark(#ffffff, #282a33);
241
- color: light-dark(#334155, #cbd5e1);
242
- cursor: pointer;
243
- white-space: nowrap;
244
- transition: all 0.12s ease;
245
- }
246
- .mod-btn:hover {
247
- border-color: #7c3aed;
248
- }
249
- .mod-btn.mod-active {
250
- background: light-dark(#6d28d9, #7c3aed);
251
- border-color: light-dark(#5b21b6, #6d28d9);
252
- color: #ffffff;
115
+ padding: 14px 16px;
116
+ border-radius: 8px;
117
+ border: 1px solid var(--border-color, #cbd5e1);
118
+ background: var(--content-background, #ffffff);
253
119
  }
254
-
255
120
  .section-title {
256
121
  font-weight: 700;
257
- font-size: 11.5px;
122
+ font-size: 12px;
258
123
  text-transform: uppercase;
259
124
  letter-spacing: 0.04em;
260
- color: light-dark(#475569, #94a3b8);
261
- border-bottom: 1px solid light-dark(#e2e8f0, #334155);
262
- padding-bottom: 4px;
263
- flex-shrink: 0;
264
- }
265
- .toolbar {
266
- @apply --horizontal;
267
- gap: 6px;
268
- align-items: center;
269
- flex-wrap: wrap;
125
+ opacity: 0.75;
126
+ border-bottom: 1px solid var(--border-color, #e2e8f0);
127
+ padding-bottom: 6px;
270
128
  }
271
- input:not([type="checkbox"]):not([type="radio"]), select, textarea {
272
- padding: 4px 8px;
273
- font-size: 12.5px;
274
- border: 1px solid light-dark(#b8b7ae, #475569);
275
- border-radius: 4px;
276
- background: light-dark(#ffffff, #15161a);
277
- color: inherit;
278
- box-sizing: border-box;
279
- }
280
- .btn {
129
+
130
+ /* Фильтры каталога */
131
+ .filter-pill {
281
132
  display: inline-flex;
282
133
  align-items: center;
283
- justify-content: center;
284
134
  gap: 5px;
285
- padding: 5px 12px;
286
- font-size: 12.5px;
287
- font-weight: 600;
288
- border: 1px solid light-dark(#b8b7ae, #4a4c56);
289
- border-radius: 4px;
290
- background: light-dark(linear-gradient(180deg, #fafaf8 0%, #e8e7de 100%), linear-gradient(180deg, #32343a 0%, #26282d 100%));
135
+ padding: 5px 11px;
136
+ border-radius: 6px;
137
+ border: 1px solid var(--border-color, #cbd5e1);
138
+ background: var(--subtle-background, #f8fafc);
291
139
  color: inherit;
140
+ font-size: 12px;
141
+ font-weight: 600;
292
142
  cursor: pointer;
293
- white-space: nowrap;
294
143
  }
295
- .btn.primary {
296
- background: linear-gradient(180deg, #6d28d9 0%, #5b21b6 100%);
297
- border-color: #4c1d95;
298
- color: #ffffff;
299
- }
300
- .btn.danger {
301
- background: #dc2626;
302
- border-color: #b91c1c;
144
+ .filter-pill.active {
145
+ background: var(--accent-color, #2563eb);
146
+ border-color: var(--accent-color, #2563eb);
303
147
  color: #ffffff;
304
148
  }
305
149
 
306
- /* Карточка модели (используем .card-active вместо глобального .selected!) */
307
- .card {
150
+ /* Строка модели (установленной или в каталоге) */
151
+ .model-row {
308
152
  @apply --vertical;
309
- flex-shrink: 0;
310
153
  gap: 6px;
311
- padding: 9px 11px;
312
- border: 1px solid light-dark(#d6d5cc, #343642);
154
+ padding: 10px 12px;
313
155
  border-radius: 6px;
314
- background: light-dark(#ffffff, #23252f);
315
- transition: border-color 0.12s ease, background 0.12s ease;
156
+ border: 1px solid var(--border-color, #e2e8f0);
157
+ background: var(--subtle-background, #f8fafc);
316
158
  }
317
- .card.clickable {
318
- cursor: pointer;
319
- }
320
- .card.clickable:hover {
321
- border-color: light-dark(#a78bfa, #8b5cf6);
322
- }
323
- .card.card-active {
324
- background: light-dark(#f5f3ff, #2b2342) !important;
325
- border: 1.5px solid #7c3aed !important;
326
- box-shadow: 0 2px 8px rgba(109, 40, 217, 0.12);
159
+ .model-row.off {
160
+ opacity: 0.55;
327
161
  }
328
- .model-top {
162
+ .model-main {
329
163
  @apply --horizontal;
330
- justify-content: space-between;
331
164
  align-items: center;
332
- gap: 8px;
165
+ justify-content: space-between;
166
+ gap: 12px;
333
167
  flex-wrap: wrap;
334
168
  }
335
- .model-title-group {
169
+ .model-left {
336
170
  @apply --horizontal;
337
171
  align-items: center;
338
- gap: 6px;
339
- flex-wrap: wrap;
172
+ gap: 10px;
340
173
  min-width: 0;
174
+ flex: 1;
341
175
  }
342
- .model-id {
176
+ .model-name {
343
177
  font-weight: 700;
344
178
  font-size: 13.5px;
345
- color: light-dark(#0f172a, #f8fafc);
346
179
  }
347
- .model-what {
180
+ .model-desc {
348
181
  font-size: 12px;
349
- line-height: 1.4;
350
- color: light-dark(#334155, #cbd5e1);
182
+ opacity: 0.8;
183
+ margin-top: 2px;
351
184
  }
352
- .caps-row {
353
- @apply --horizontal;
185
+ .mem-tag {
186
+ display: inline-flex;
354
187
  align-items: center;
355
188
  gap: 5px;
356
- flex-wrap: wrap;
357
- }
358
- .caps-label {
359
- font-size: 11px;
360
- font-weight: 700;
361
- color: light-dark(#64748b, #94a3b8);
362
- margin-right: 2px;
189
+ font-size: 12px;
190
+ color: inherit;
191
+ opacity: 0.85;
192
+ white-space: nowrap;
363
193
  }
194
+
364
195
  .progress-track {
365
196
  width: 100%;
366
- height: 8px;
367
- background: light-dark(#e2e8f0, #334155);
368
- border-radius: 4px;
197
+ height: 6px;
198
+ background: var(--border-color, #e2e8f0);
199
+ border-radius: 3px;
369
200
  overflow: hidden;
370
201
  }
371
202
  .progress-fill {
372
203
  height: 100%;
373
- background: linear-gradient(90deg, #7c3aed, #2563eb);
204
+ background: var(--accent-color, #2563eb);
374
205
  transition: width 0.2s ease;
375
206
  }
376
- .form-grid {
207
+
208
+ .bios-box {
209
+ padding: 10px 14px;
210
+ border-radius: 6px;
211
+ border: 1px solid var(--border-color, #cbd5e1);
212
+ background: var(--subtle-background, #f8fafc);
213
+ font-size: 12.5px;
214
+ line-height: 1.5;
215
+ }
216
+
217
+ .grid-2 {
377
218
  display: grid;
378
- grid-template-columns: repeat(2, minmax(140px, 1fr));
379
- gap: 8px;
219
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
220
+ gap: 14px;
380
221
  }
381
- .field-box {
222
+ .field {
382
223
  @apply --vertical;
383
- gap: 3px;
224
+ gap: 4px;
384
225
  }
385
- .field-box label {
386
- font-size: 11.5px;
226
+ .field label {
227
+ font-size: 12px;
387
228
  font-weight: 600;
388
- color: light-dark(#475569, #94a3b8);
389
229
  }
390
- .chart-box {
391
- padding: 8px;
392
- border: 1px solid light-dark(#cbd5e1, #334155);
393
- border-radius: 4px;
394
- background: light-dark(#ffffff, #15161a);
395
- }
396
- .file-list {
397
- max-height: 170px;
398
- overflow-y: auto;
399
- margin-top: 4px;
400
- border-top: 1px solid light-dark(#e2e8f0, #334155);
401
- padding-top: 4px;
230
+ input:not([type="checkbox"]), select {
231
+ padding: 6px 10px;
232
+ font-size: 13px;
233
+ border: 1px solid var(--border-color, #cbd5e1);
234
+ border-radius: 6px;
235
+ background: var(--content-background, #ffffff);
236
+ color: inherit;
402
237
  }
403
- .file-row {
404
- display: flex;
405
- justify-content: space-between;
238
+ .btn {
239
+ display: inline-flex;
406
240
  align-items: center;
407
- gap: 8px;
408
- font-size: 11.5px;
409
- padding: 2px 0;
241
+ gap: 6px;
242
+ padding: 6px 13px;
243
+ font-size: 12.5px;
244
+ font-weight: 600;
245
+ border-radius: 6px;
246
+ border: 1px solid var(--border-color, #cbd5e1);
247
+ background: var(--content-background, #ffffff);
248
+ color: inherit;
249
+ cursor: pointer;
250
+ white-space: nowrap;
251
+ }
252
+ .btn.primary {
253
+ background: var(--accent-color, #2563eb);
254
+ border-color: var(--accent-color, #2563eb);
255
+ color: #ffffff;
256
+ }
257
+ .btn.danger {
258
+ color: light-dark(#b91c1c, #fca5a5);
259
+ border-color: light-dark(#fecaca, #7f1d1d);
260
+ }
261
+ .msg {
262
+ padding: 8px 12px;
263
+ border-radius: 6px;
264
+ font-size: 12.5px;
265
+ }
266
+ .msg.ok {
267
+ background: light-dark(#dcfce7, #14532d);
268
+ color: light-dark(#166534, #bbf7d0);
410
269
  }
411
- .mono {
412
- font-family: Consolas, monospace;
413
- font-size: 11.5px;
414
- white-space: pre-wrap;
415
- word-break: break-word;
270
+ .msg.err {
271
+ background: light-dark(#fee2e2, #450a0a);
272
+ color: light-dark(#991b1b, #fecaca);
416
273
  }
417
274
  </style>
418
275
 
419
- <!-- Верхний баннер реального аппаратного профиля хоста -->
420
- <div class="hw-banner">
421
- <div class="hw-left">
422
- <div class="hw-title">Локальные модели ИИ и дообучение (LoRA / SFT / GRPO)</div>
423
- <div class="hw-specs">
424
- <span class="hw-chip">CPU: <strong>{{hardware?.cpuModel || 'Определение…'}}</strong> ({{hardware?.cpuCores || 0}} потоков)</span>
425
- <span class="hw-chip">ОЗУ: <strong>{{hardware?.totalRamGb || 0}} ГБ</strong> (свободно {{hardware?.freeRamGb || 0}} ГБ)</span>
426
- <span class="hw-chip">Бюджет под ИИ: <strong>~{{hardware?.usableMemoryGb || 0}} ГБ</strong></span>
276
+ <!-- Компактная шапка состояния локального ИИ и памяти -->
277
+ <div class="header-card">
278
+ <div class="header-left">
279
+ <oda-icon icon="carbon:chip" icon-size="30"></oda-icon>
280
+ <div>
281
+ <div class="title">Локальный ИИ</div>
282
+ <div class="hw-line">
283
+ {{hardware?.cpuModel || 'Процессор'}} · ОЗУ {{hardware?.totalRamGb || 0}} ГБ (доступно под ИИ ~{{hardware?.usableMemoryGb || 0}} ГБ)
284
+ </div>
427
285
  </div>
428
286
  </div>
429
- <div class="hw-badges">
430
- <span class="badge purple">Локальных моделей: {{localModels.length}}</span>
431
- <span class="badge purple">LoRA-адаптеров: {{adapters.length}}</span>
432
- <span class="badge">Датасетов WORK: {{datasets.length}}</span>
287
+ <div class="toolbar">
288
+ <span class="badge" ~class="runtime?.running ? 'active' : 'neutral'">
289
+ <span class="dot" ~class="runtime?.running ? 'on' : 'off'"></span>
290
+ <span>{{runtime?.modeLabel || 'Автозапуск по запросу'}}</span>
291
+ </span>
292
+ <button type="button" class="btn" ~if="runtime?.running" @tap="releaseMemory()">Освободить память</button>
433
293
  </div>
434
294
  </div>
435
295
 
296
+ <div class="msg ok" ~if="notice">{{notice}}</div>
297
+ <div class="msg err" ~if="error">{{error}}</div>
298
+
299
+ <!-- Две чистые вкладки -->
436
300
  <div class="tabs-bar">
437
- <button type="button" class="tab-btn" ~class="{'tab-active': activeTab === 'hf'}" @tap="activeTab = 'hf'">1. Каталог моделей Hugging Face ({{hfModels.length}})</button>
438
- <button type="button" class="tab-btn" ~class="{'tab-active': activeTab === 'dataset'}" @tap="activeTab = 'dataset'">2. Датасеты WORK ({{datasets.length}})</button>
439
- <button type="button" class="tab-btn" ~class="{'tab-active': activeTab === 'train'}" @tap="activeTab = 'train'">3. Дообучение (LoRA / SFT / GRPO)</button>
440
- <button type="button" class="tab-btn" ~class="{'tab-active': activeTab === 'halogen'}" @tap="openHalogenTab()">4. Контур Halogen (Ryzen AI 192 ГБ: GPU + NPU)</button>
301
+ <button type="button" class="tab-btn" ~class="{'tab-active': activeTab === 'models'}" @tap="activeTab = 'models'">
302
+ Модели ({{localModels.length}})
303
+ </button>
304
+ <button type="button" class="tab-btn" ~class="{'tab-active': activeTab === 'train'}" @tap="activeTab = 'train'">
305
+ Дообучение ({{adapters.length}})
306
+ </button>
441
307
  </div>
442
308
 
443
- <div class="error" ~if="error">{{error}}</div>
309
+ <!-- ВКЛАДКА 1: МОДЕЛИ (УСТАНОВЛЕННЫЕ + КАТАЛОГ) -->
310
+ <div ~if="activeTab === 'models'" class="vertical" style="gap:14px;">
444
311
 
445
- <!-- ВКЛАДКА 1: КАТАЛОГ МОДЕЛЕЙ HUGGING FACE -->
446
- <div class="workspace" ~if="activeTab === 'hf'">
447
- <div class="panel-left">
448
- <!-- Закреплённая сверху панель выбора типа модели, умений и поиска -->
449
- <div class="sticky-header">
450
- <div class="modality-bar">
451
- <button type="button" class="mod-btn" ~for="modalityTabs"
452
- ~class="{'mod-active': hfModality === $for.item.id}"
453
- @tap="setModality($for.item.id)">
454
- <oda-icon :icon="$for.item.icon" icon-size="15"></oda-icon>
455
- <span>{{$for.item.label}}</span>
456
- </button>
457
- </div>
458
- <div class="toolbar">
459
- <input style="flex:1;min-width:180px;" :value="hfQuery" placeholder="Поиск модели на Hugging Face (например: Qwen3, Whisper, FLUX, Wan2.1, BGE)…" @input="hfQuery = $this.value" @keydown.enter="searchHf()">
460
- <select title="Фильтр по умению модели" @change="setCapability($this.value)">
461
- <option ~for="capabilityOptions" :value="$for.item.id" :selected="hfCapability === $for.item.id">{{$for.item.label}}</option>
462
- </select>
463
- <select title="Совместимость и формат" @change="setHfFilter($this.value)">
464
- <option value="all" :selected="hfFilter === 'all'">Все по памяти</option>
465
- <option value="fit" :selected="hfFilter === 'fit'">Подходящие моей системе</option>
466
- <option value="gguf" :selected="hfFilter === 'gguf'">Формат GGUF</option>
467
- </select>
468
- <button type="button" class="btn primary" @tap="searchHf()">Найти</button>
469
- </div>
312
+ <!-- 1. Установленные локальные модели -->
313
+ <div class="card">
314
+ <div class="section-title">Установленные модели в /MODELS/Local ({{localModels.length}})</div>
315
+
316
+ <div ~if="!localModels.length" style="opacity:0.75;padding:4px 0;">
317
+ Пока не установлено ни одной локальной модели. Выберите подходящую модель в каталоге ниже и нажмите «Установить».
470
318
  </div>
471
319
 
472
- <!-- Прокручиваемый список карточек моделей -->
473
- <div class="cards-scroll">
474
- <div class="card clickable" ~for="hfModels"
475
- ~class="{'card-active': selectedRepo?.id === $for.item.id}"
476
- @tap="inspectRepo($for.item.id)">
477
- <div class="model-top">
478
- <div class="model-title-group">
479
- <oda-icon :icon="$for.item.icon || 'ai:ollama'" icon-size="20" style="flex-shrink:0;"></oda-icon>
480
- <span class="model-id">{{$for.item.id}}</span>
481
- <span class="badge mod">{{$for.item.modalityLabel || $for.item.modality}}</span>
482
- <span class="badge optimal" ~if="$for.item.fit === 'optimal'">Подходит (обучение + запуск)</span>
483
- <span class="badge warn" ~if="$for.item.fit === 'inference_only'">Доступен запуск</span>
484
- <span class="badge heavy" ~if="$for.item.fit === 'too_large'">Мало памяти</span>
485
- </div>
486
- <div class="hw-badges">
487
- <span class="badge" ~if="$for.item.ramRequiredGb">Запуск: ~{{$for.item.ramRequiredGb}} ГБ</span>
488
- <span class="badge" ~if="$for.item.trainRamGb && $for.item.capabilities?.includes('lora')">LoRA: ~{{$for.item.trainRamGb}} ГБ</span>
489
- <span class="badge">{{$for.item.format}}</span>
320
+ <div class="model-row" ~for="localModels" ~class="{'off': !$for.item.active}">
321
+ <div class="model-main">
322
+ <div class="model-left">
323
+ <span class="dot" ~class="$for.item.active ? 'on' : 'off'"></span>
324
+ <oda-icon :icon="$for.item.icon || 'ai:ollama'" icon-size="20"></oda-icon>
325
+ <div style="min-width:0;">
326
+ <div class="model-name">{{$for.item.label || $for.item.id}}</div>
327
+ <div class="model-desc">
328
+ Путь: <code>{{$for.item.path}}</code>
329
+ <span ~if="$for.item.sizeGb"> · На диске: {{$for.item.sizeGb}} ГБ</span>
330
+ </div>
490
331
  </div>
491
332
  </div>
492
- <div class="model-what">{{$for.item.whatItDoes || $for.item.recommendedFor}}</div>
493
- <div class="caps-row" ~if="$for.item.capabilityBadges?.length">
494
- <span class="caps-label">Умеет:</span>
495
- <span class="cap-pill" ~for="$for.item.capabilityBadges" :title="$for.$for.item.desc">{{$for.$for.item.label}}</span>
333
+ <div class="toolbar">
334
+ <label class="toolbar" style="cursor:pointer;font-size:12px;font-weight:600;">
335
+ <input type="checkbox" :checked="$for.item.enabled !== false" @change="toggleLocalModel($for.item.path, $event.target.checked)">
336
+ <span>Вкл</span>
337
+ </label>
338
+ <button type="button" class="btn danger" @tap="deleteLocalModel($for.item.id)">Удалить</button>
496
339
  </div>
497
340
  </div>
498
341
  </div>
499
342
  </div>
500
343
 
501
- <div class="panel-right">
502
- <div class="section-title">Паспорт репозитория и загрузка</div>
503
- <div class="card" ~if="selectedRepo">
504
- <div class="model-top">
505
- <div class="model-title-group">
506
- <oda-icon :icon="selectedRepo.icon || 'ai:ollama'" icon-size="22" style="flex-shrink:0;"></oda-icon>
507
- <span class="model-id">{{selectedRepo.id}}</span>
508
- </div>
509
- <span class="badge mod">{{selectedRepo.modality || 'llm'}}</span>
510
- </div>
511
- <div class="model-what" ~if="selectedRepo.whatItDoes">{{selectedRepo.whatItDoes}}</div>
512
- <div class="caps-row" ~if="selectedRepo.capabilityBadges?.length">
513
- <span class="caps-label">Возможности:</span>
514
- <span class="cap-pill" ~for="selectedRepo.capabilityBadges" :title="$for.item.desc">{{$for.item.label}}</span>
515
- </div>
516
- <div style="font-size:12px;margin-top:2px;">
517
- Вес файлов: <strong>~{{selectedRepo.totalWeightGb}} ГБ</strong> · Файлов: <strong>{{selectedRepo.files?.length || 0}}</strong>
518
- </div>
519
- <div style="font-size:11.5px;opacity:0.9;" ~if="selectedRepo.fitLabel">{{selectedRepo.fitLabel}}</div>
520
- <div class="field-box" ~if="selectedRepo.quants?.length">
521
- <label>Квантизация / Формат весов:</label>
522
- <select @change="selectedQuant = $this.value">
523
- <option ~for="selectedRepo.quants" :value="$for.item" :selected="$for.item === selectedQuant">{{$for.item}}</option>
524
- </select>
525
- </div>
526
- <div class="toolbar" style="margin-top:4px;">
527
- <button type="button" class="btn primary" @tap="downloadRepo(selectedRepo.id)">Скачать с Hugging Face в /MODELS/Local</button>
528
- <input style="width:140px;" type="password" :value="hfToken" placeholder="HF Token (опц.)" @change="hfToken = $this.value">
529
- </div>
530
- <div class="file-list" ~if="selectedRepo.files?.length">
531
- <div class="file-row" ~for="selectedRepo.files">
532
- <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{$for.item.name}}</span>
533
- <span style="flex-shrink:0;font-weight:600;">{{$for.item.sizeFormatted || ($for.item.sizeGb + ' ГБ')}}</span>
344
+ <!-- 2. Активные загрузки (если идут прямо сейчас) -->
345
+ <div class="card" ~if="activeDownloads.length">
346
+ <div class="section-title">Идёт установка ({{activeDownloads.length}})</div>
347
+ <div class="model-row" ~for="activeDownloads">
348
+ <div class="model-main">
349
+ <div class="model-name">{{$for.item.repoId}}</div>
350
+ <div class="toolbar">
351
+ <span style="font-size:12px;font-weight:600;">{{$for.item.progress || 0}}% · {{$for.item.speedMBs || 0}} МБ/с</span>
352
+ <button type="button" class="btn danger" @tap="cancelDownload($for.item.id)">Отменить</button>
534
353
  </div>
535
354
  </div>
536
- </div>
537
-
538
- <div class="section-title">Загрузки ({{downloads.length}})</div>
539
- <div class="card" ~for="downloads">
540
- <div class="model-top">
541
- <strong>{{$for.item.repoId}}</strong>
542
- <span class="badge purple">{{$for.item.status}} · {{$for.item.progress}}%</span>
543
- </div>
544
- <div style="font-size:11.5px;">Файл: {{$for.item.currentFile}} · Скорость: {{$for.item.speedMBs}} МБ/с</div>
545
355
  <div class="progress-track">
546
356
  <div class="progress-fill" ~style="{width: ($for.item.progress || 0) + '%'}"></div>
547
357
  </div>
548
- <div style="font-size:11.5px;" ~if="$for.item.modelPath">Узел в дереве: <strong>{{$for.item.modelPath}}</strong></div>
549
- <div class="error" ~if="$for.item.error">{{$for.item.error}}</div>
550
- <div class="toolbar" ~if="$for.item.status === 'downloading'">
551
- <button type="button" class="btn danger" @tap="cancelDownload($for.item.id)">Отменить</button>
552
- </div>
553
- </div>
554
-
555
- <div class="section-title">Установленные модели в /MODELS/Local ({{localModels.length}})</div>
556
- <div class="card" ~for="localModels">
557
- <div class="model-top">
558
- <strong>{{$for.item.id}}</strong>
559
- <span class="badge">{{$for.item.path}}</span>
560
- </div>
561
- <div style="font-size:11.5px;">Файлы: {{$for.item.weightFiles?.join(', ') || 'метаданные $ai'}}</div>
562
358
  </div>
563
359
  </div>
564
- </div>
565
360
 
566
- <!-- ВКЛАДКА 2: ДАТАСЕТЫ WORK -->
567
- <div class="workspace" ~if="activeTab === 'dataset'">
568
- <div class="panel-left">
569
- <div class="sticky-header">
570
- <div class="section-title">Сборка обучающего датасета из живых данных WORK (ChatML JSONL)</div>
571
- <div class="form-grid">
572
- <div class="field-box">
573
- <label>Имя датасета (.jsonl)</label>
574
- <input :value="datasetName" @input="datasetName = $this.value">
575
- </div>
576
- <div class="field-box">
577
- <label>Организация ($base)</label>
578
- <input :value="datasetBasePath" @input="datasetBasePath = $this.value">
579
- </div>
580
- </div>
581
- <div class="toolbar" style="gap:14px;">
582
- <label><input type="checkbox" :checked="srcTasks" @change="srcTasks = $this.checked"> Задачи ИИ и Консультанта (*.task)</label>
583
- <label><input type="checkbox" :checked="srcAccounting" @change="srcAccounting = $this.checked"> Операции, проводки и ОСВ (work.db)</label>
584
- <label><input type="checkbox" :checked="srcClasses" @change="srcClasses = $this.checked"> Классы, контракты и навыки (/MODULES)</label>
585
- </div>
361
+ <!-- 3. Каталог моделей для установки в 1 клик -->
362
+ <div class="card">
363
+ <div class="section-title">Каталог моделей для установки</div>
364
+
365
+ <!-- Панель категорий и поиска -->
366
+ <div class="toolbar" style="justify-content:space-between;">
586
367
  <div class="toolbar">
587
- <button type="button" class="btn primary" @tap="buildDataset()">Собрать датасет WORK</button>
368
+ <button type="button" class="filter-pill" ~for="categoryTabs"
369
+ ~class="{'active': hfModality === $for.item.id}"
370
+ @tap="setModality($for.item.id)">
371
+ <oda-icon :icon="$for.item.icon" icon-size="14"></oda-icon>
372
+ <span>{{$for.item.label}}</span>
373
+ </button>
588
374
  </div>
589
- </div>
590
-
591
- <div class="cards-scroll">
592
- <div class="section-title">Собранные датасеты ({{datasets.length}})</div>
593
- <div class="card clickable" ~for="datasets" ~class="{'card-active': selectedDataset?.name === $for.item.name}" @tap="selectedDataset = $for.item">
594
- <div class="model-top">
595
- <strong>{{$for.item.name}}</strong>
596
- <span class="badge purple">Примеров: {{$for.item.totalExamples}} · ~{{$for.item.estimatedTokens}} токенов</span>
597
- </div>
598
- <div style="font-size:11.5px;">
599
- Файл: <code>{{$for.item.filePath}}</code> · Источники: задачи ({{$for.item.bySource?.tasks || 0}}), учёт ({{$for.item.bySource?.accounting || 0}}), классы ({{$for.item.bySource?.classes || 0}})
600
- </div>
375
+ <div class="toolbar" ~if="hfModality !== 'halo'">
376
+ <input style="width:220px;" :value="hfQuery" placeholder="Поиск на Hugging Face (Qwen3, Whisper, FLUX)…" @input="hfQuery = $this.value" @keydown.enter="searchHf()">
377
+ <button type="button" class="btn" @tap="searchHf()">Найти</button>
378
+ <label class="toolbar" style="cursor:pointer;font-size:12px;">
379
+ <input type="checkbox" :checked="hfFilter === 'fit'" @change="setHfFilter($event.target.checked ? 'fit' : 'all')">
380
+ <span>Только подходящие по памяти</span>
381
+ </label>
601
382
  </div>
602
383
  </div>
603
- </div>
604
384
 
605
- <div class="panel-right">
606
- <div class="section-title">Предпросмотр обучающих примеров (ShareGPT / ChatML)</div>
607
- <div class="card" ~for="selectedDataset?.preview">
608
- <div class="model-top">
609
- <strong>{{$for.item.id}}</strong>
610
- <span class="badge">{{$for.item.category}} · {{$for.item.tokens}} tok</span>
385
+ <!-- Режим комплекта Halo (AMD Ryzen AI 128–192 ГБ) + краткая памятка по BIOS -->
386
+ <div ~if="hfModality === 'halo'" class="vertical" style="gap:10px;">
387
+ <div class="bios-box">
388
+ <div class="model-main">
389
+ <div>
390
+ <strong>Настройка BIOS и выделения памяти для машинок Halo (AMD Ryzen™ AI Max+ PRO 395/495 · 128–192 ГБ):</strong>
391
+ <div style="margin-top:4px;">
392
+ • <b>BIOS → UMA Frame Buffer Size (Variable Graphics Memory):</b> на Windows 11 выделите под iGPU <b>128 ГБ</b> (на системах 192 ГБ) или <b>96 ГБ</b> (на системах 128 ГБ); на Linux оставьте <b>512 МБ</b> (память выделяется динамически через GTT).<br>
393
+ • <b>BIOS → NPU / IPU:</b> убедитесь, что нейронный сопроцессор Ryzen AI XDNA2 включён (<code>Enabled</code>).<br>
394
+ • <b>Файл подкачки Windows 11:</b> рекомендуется фиксированный PageFile <b>128 ГБ</b> на NVMe SSD.
395
+ </div>
396
+ </div>
397
+ <button type="button" class="btn primary" @tap="downloadHalogenBundleAll()">
398
+ Установить весь комплект Halo (GPU + NPU)
399
+ </button>
400
+ </div>
611
401
  </div>
612
- <div style="font-size:12px;"><strong>User:</strong> {{$for.item.userPrompt}}</div>
613
- <div class="mono" style="background:rgba(128,128,128,0.08);padding:6px;border-radius:4px;">{{$for.item.assistantReply}}</div>
614
- </div>
615
- </div>
616
- </div>
617
402
 
618
- <!-- ВКЛАДКА 3: ДООБУЧЕНИЕ (LoRA / SFT / GRPO) -->
619
- <div class="workspace" ~if="activeTab === 'train'">
620
- <div class="panel-left">
621
- <div class="sticky-header">
622
- <div class="section-title">Параметры дообучения (адаптированы под доступные ~{{hardware?.usableMemoryGb || 0}} ГБ памяти)</div>
623
- <div class="form-grid">
624
- <div class="field-box">
625
- <label>Имя обучаемой модели / LoRA-адаптера</label>
626
- <input :value="trainAdapterName" @input="trainAdapterName = $this.value">
627
- </div>
628
- <div class="field-box">
629
- <label>Базовая модель (рекомендована: {{hardware?.recommendedBaseModel || 'Qwen/Qwen3-14B'}})</label>
630
- <input :value="trainBaseModel" @input="trainBaseModel = $this.value">
631
- </div>
632
- <div class="field-box">
633
- <label>Датасет (.jsonl)</label>
634
- <input :value="trainDatasetName" @input="trainDatasetName = $this.value">
635
- </div>
636
- <div class="field-box">
637
- <label>Метод обучения</label>
638
- <select @change="trainMode = $this.value">
639
- <option value="sft" :selected="trainMode === 'sft'">SFT (Supervised Fine-Tuning на примерах)</option>
640
- <option value="grpo" :selected="trainMode === 'grpo'">GRPO (RL с верификатором учётного ядра WORK)</option>
641
- </select>
642
- </div>
643
- <div class="field-box">
644
- <label>Ранг LoRA (r)</label>
645
- <select @change="trainLoraR = Number($this.value)">
646
- <option value="8" :selected="trainLoraR === 8">r = 8 (компактный)</option>
647
- <option value="16" :selected="trainLoraR === 16">r = 16 (стандарт)</option>
648
- <option value="32" :selected="trainLoraR === 32">r = 32 (глубокая адаптация)</option>
649
- <option value="64" :selected="trainLoraR === 64">r = 64 (максимальный)</option>
650
- </select>
403
+ <div class="model-row" ~for="halogenCatalog">
404
+ <div class="model-main">
405
+ <div class="model-left">
406
+ <oda-icon :icon="$for.item.icon || 'ai:qwen'" icon-size="20"></oda-icon>
407
+ <div style="min-width:0;">
408
+ <div class="model-name">{{$for.item.label || $for.item.nodeName}}</div>
409
+ <div class="model-desc">{{$for.item.role}}</div>
410
+ </div>
411
+ </div>
412
+ <div class="toolbar">
413
+ <span class="mem-tag">
414
+ <span class="dot on"></span>
415
+ <span>{{$for.item.device}} · ~{{$for.item.ramGb}} ГБ</span>
416
+ </span>
417
+ <span class="badge active" ~if="$for.item.weightsDownloaded">Установлена</span>
418
+ <button type="button" class="btn primary" ~if="!$for.item.weightsDownloaded && !$for.item.activeDownload" @tap="downloadHalogenModel($for.item.nodeName)">
419
+ Установить
420
+ </button>
421
+ </div>
651
422
  </div>
652
- <div class="field-box">
653
- <label>Длина контекста (max_seq_length)</label>
654
- <select @change="trainMaxSeq = Number($this.value)">
655
- <option value="4096" :selected="trainMaxSeq === 4096">4 096 токенов</option>
656
- <option value="8192" :selected="trainMaxSeq === 8192">8 192 токена</option>
657
- <option value="16384" :selected="trainMaxSeq === 16384">16 384 токена</option>
658
- <option value="32768" :selected="trainMaxSeq === 32768">32 768 токенов</option>
659
- </select>
423
+ <div class="progress-track" ~if="$for.item.activeDownload">
424
+ <div class="progress-fill" ~style="{width: ($for.item.activeDownload.progress || 0) + '%'}"></div>
660
425
  </div>
661
426
  </div>
662
- <div class="toolbar">
663
- <button type="button" class="btn primary" @tap="startTraining()">Запустить дообучение</button>
664
- </div>
665
427
  </div>
666
428
 
667
- <div class="cards-scroll">
668
- <div class="section-title">Монитор задач обучения ({{jobs.length}})</div>
669
- <div class="card" ~for="jobs">
670
- <div class="model-top">
671
- <strong>{{$for.item.adapterName}} ({{$for.item.mode.toUpperCase()}})</strong>
672
- <span class="badge purple">{{$for.item.status}} · шаг {{$for.item.step}} / {{$for.item.totalSteps}}</span>
673
- </div>
674
- <div style="font-size:12px;">
675
- База: <code>{{$for.item.baseModel}}</code> · Loss: <strong>{{$for.item.loss}}</strong>
676
- <span ~if="$for.item.reward !== null"> · GRPO Reward: <strong>{{$for.item.reward}}</strong></span>
677
- · Память: <strong>{{$for.item.ramUsedGb}} / {{$for.item.usableMemoryGb || hardware?.usableMemoryGb || 0}} ГБ</strong> · Скорость: {{$for.item.tokensPerSec}} tok/s
678
- </div>
679
- <div class="progress-track">
680
- <div class="progress-fill" ~style="{width: Math.round(($for.item.step / ($for.item.totalSteps || 1)) * 100) + '%'}"></div>
681
- </div>
682
- <div style="font-size:12px;" ~if="$for.item.publishedPath">
683
- Опубликована в дереве: <strong>{{$for.item.publishedPath}}</strong>
429
+ <!-- Обычный каталог моделей (Чат, Зрение, Речь, Картинки, Поиск) -->
430
+ <div ~if="hfModality !== 'halo'" class="vertical" style="gap:8px;">
431
+ <div class="model-row" ~for="hfModels">
432
+ <div class="model-main">
433
+ <div class="model-left">
434
+ <oda-icon :icon="$for.item.icon || 'ai:ollama'" icon-size="20"></oda-icon>
435
+ <div style="min-width:0;">
436
+ <div class="model-name">{{$for.item.id}}</div>
437
+ <div class="model-desc">{{$for.item.whatItDoes || $for.item.recommendedFor}}</div>
438
+ </div>
439
+ </div>
440
+ <div class="toolbar">
441
+ <span class="mem-tag" :title="$for.item.fit === 'too_large' ? 'Требуется больше памяти, чем доступно на этом ПК' : 'Подходит по памяти'">
442
+ <span class="dot" ~class="$for.item.fit === 'too_large' ? 'off' : ($for.item.fit === 'optimal' ? 'on' : 'warn')"></span>
443
+ <span>~{{$for.item.ramRequiredGb || 4}} ГБ</span>
444
+ </span>
445
+ <span class="badge active" ~if="isRepoInstalled($for.item.id)">Установлена</span>
446
+ <button type="button" class="btn primary" ~if="!isRepoInstalled($for.item.id) && !getActiveDownload($for.item.id)" @tap="installModelOneClick($for.item.id)">
447
+ Установить
448
+ </button>
449
+ </div>
684
450
  </div>
685
- <div class="toolbar" ~if="$for.item.status === 'running'">
686
- <button type="button" class="btn danger" @tap="stopTraining($for.item.id)">Остановить</button>
451
+ <div class="progress-track" ~if="getActiveDownload($for.item.id)">
452
+ <div class="progress-fill" ~style="{width: (getActiveDownload($for.item.id)?.progress || 0) + '%'}"></div>
687
453
  </div>
688
454
  </div>
689
455
  </div>
690
456
  </div>
457
+ </div>
691
458
 
692
- <div class="panel-right">
693
- <div class="section-title">Кривая обучения (Loss) и Верификатор GRPO</div>
694
- <div class="chart-box" ~if="activeJob?.history?.length">
695
- <div style="font-size:12px;font-weight:600;margin-bottom:4px;">
696
- График падения Loss ({{activeJob.adapterName}}): {{activeJob.history[0]?.loss}} → {{activeJob.loss}}
697
- </div>
698
- <svg viewBox="0 0 320 110" style="width:100%;height:110px;display:block;">
699
- <polyline fill="none" stroke="#7c3aed" stroke-width="2.5" :points="lossPolyline"></polyline>
700
- </svg>
459
+ <!-- ВКЛАДКА 2: ДООБУЧЕНИЕ НА ДАННЫХ ОРГАНИЗАЦИИ (2 ШАГА) -->
460
+ <div ~if="activeTab === 'train'" class="grid-2">
461
+ <!-- Шаг 1: Сборка данных организации -->
462
+ <div class="card">
463
+ <div class="section-title">Шаг 1. Собрать обучающий набор из организации</div>
464
+ <div class="field">
465
+ <label>Организация ($base)</label>
466
+ <input :value="datasetBasePath" placeholder="/ORGANIZATIONS/Моё предприятие" @input="datasetBasePath = $this.value">
701
467
  </div>
702
-
703
- <div class="section-title">Песочница Верификатора GRPO (Reward Function учётного ядра)</div>
704
- <div class="card">
705
- <div style="font-size:11.5px;opacity:0.85;">
706
- Проверяет ответ модели через <code>checkSchema</code> и <code>previewEntries</code> в БД организации и начисляет награду <code>reward (0..1)</code>.
707
- </div>
708
- <textarea rows="5" class="mono" :value="verifySample" @input="verifySample = $this.value"></textarea>
709
- <div class="toolbar">
710
- <button type="button" class="btn" @tap="runVerify()">Проверить ответ (GRPO Reward)</button>
711
- <span class="badge purple" ~if="verifyResult">Reward: {{verifyResult.reward}} ({{verifyResult.valid ? 'валидно' : 'ошибка'}})</span>
712
- </div>
713
- <div ~if="verifyResult?.reasons?.length" style="font-size:11.5px;">
714
- <div ~for="verifyResult.reasons">• {{$for.item}}</div>
715
- </div>
468
+ <div class="field">
469
+ <label>Имя обучающего набора (.jsonl)</label>
470
+ <input :value="datasetName" @input="datasetName = $this.value">
716
471
  </div>
717
- </div>
718
- </div>
719
-
720
- <!-- ВКЛАДКА 4: ГИБРИДНЫЙ КОНТУР HALOGEN (AMD RYZEN AI 495 PRO 192 GB: GPU + NPU) -->
721
- <div class="workspace" ~if="activeTab === 'halogen'">
722
- <div class="panel-left">
723
- <div class="sticky-header">
724
- <div class="section-title">Контур Strix Halo (:8731 · Linux halogen-flash-server / Windows 11 Halowin Bubo · Gufo ROCm 10.0 gfx1151 + NPU)</div>
725
- <div class="model-top">
726
- <div class="model-title-group">
727
- <span class="badge optimal" ~if="halogenState?.online">Сервер :8731 активен</span>
728
- <span class="badge purple" ~if="!halogenState?.online">Автономный контур WORK</span>
729
- <strong>{{halogenState?.modeLabel || 'Контур готов'}}</strong>
730
- </div>
731
- <div class="toolbar">
732
- <button type="button" class="btn" @tap="refreshHalogen()">Проверить :8731</button>
733
- <button type="button" class="btn primary" ~if="halogenState?.windowsEnv?.isWindows && !halogenState?.windowsEnv?.rocmRuntimePrepared" @tap="prepareWindowsBubo('runtime')">Установить Halowin Bubo (ROCm 10.0 gfx1151)</button>
734
- <button type="button" class="btn primary" @tap="downloadHalogenBundleAll()">Скачать весь пакет (GPU + NPU)</button>
735
- <button type="button" class="btn primary" @tap="startHalogenServer()">Запустить локальный сервер</button>
736
- </div>
737
- </div>
738
- <div class="hw-specs" ~if="halogenState?.memoryPlan">
739
- <span class="hw-chip">Пул KV: <strong>{{halogenState?.cache?.pool?.used || 0}} / {{halogenState.memoryPlan.kvPoolPositions}} tok</strong> ({{halogenState.memoryPlan.kvSlots}} слота)</span>
740
- <span class="hw-chip">Веса Flash: <strong>{{halogenState.memoryPlan.flashWeightsGb}} ГБ</strong> + KV <strong>{{halogenState.memoryPlan.deviceSideGb}} ГБ</strong></span>
741
- <span class="hw-chip">FP8 Lookup Table: <strong>{{halogenState.memoryPlan.lookupTableGb}} ГБ</strong> (в ОЗУ)</span>
742
- <span class="hw-chip">NPU Sidecar: <strong>{{halogenState.memoryPlan.npuSidecarGb}} ГБ</strong></span>
743
- <span class="hw-chip">Свободно под WORK: <strong>~{{halogenState.memoryPlan.hostFreeForWorkGb}} ГБ</strong></span>
744
- </div>
745
- <div class="hw-specs" ~if="halogenState?.windowsEnv?.isWindows">
746
- <span class="hw-chip">Движок Windows: <strong>{{halogenState.windowsEnv.buboInstalled ? 'Halowin Bubo v0.1.2 (gufo 0.26.0)' : 'Не установлен'}}</strong></span>
747
- <span class="hw-chip">ROCm 10.0 (gfx1151): <strong>{{halogenState.windowsEnv.rocmRuntimePrepared ? 'Установлен' : 'Требуется установка'}}</strong></span>
748
- <span class="hw-chip">NPU XRT (xrt-smi): <strong>{{halogenState.windowsEnv.npuDriverInstalled ? 'Активен' : 'Не найден'}}</strong></span>
749
- <span class="hw-chip">VC++ Redist: <strong>{{halogenState.windowsEnv.vcRedistInstalled ? 'ОК' : 'Требуется'}}</strong></span>
750
- <span class="hw-chip" ~if="halogenState.windowsEnv.pageFileGb !== null">PageFile: <strong>{{halogenState.windowsEnv.pageFileGb}} ГБ</strong> (рек. 128 ГБ)</span>
751
- </div>
472
+ <div class="vertical" style="gap:6px;margin-top:2px;">
473
+ <label class="toolbar" style="cursor:pointer;"><input type="checkbox" :checked="srcTasks" @change="srcTasks = $event.target.checked"> <span>Диалоги и решённые задачи ИИ (*.task)</span></label>
474
+ <label class="toolbar" style="cursor:pointer;"><input type="checkbox" :checked="srcAccounting" @change="srcAccounting = $event.target.checked"> <span>Учётные операции, проводки и ведомости (work.db)</span></label>
475
+ <label class="toolbar" style="cursor:pointer;"><input type="checkbox" :checked="srcClasses" @change="srcClasses = $event.target.checked"> <span>Структура классов и справочников (/MODULES)</span></label>
476
+ </div>
477
+ <div class="toolbar" style="margin-top:4px;">
478
+ <button type="button" class="btn primary" @tap="buildDataset()">Собрать обучающий набор</button>
752
479
  </div>
753
480
 
754
- <div class="cards-scroll">
755
- <div class="section-title">Матрица моделей /MODELS/Halogen ({{halogenCatalog.length}})</div>
756
- <div class="card" ~for="halogenCatalog">
757
- <div class="model-top">
758
- <div class="model-title-group">
759
- <strong>{{$for.item.nodeName}}</strong>
760
- <span class="badge mod">{{$for.item.model}}</span>
761
- <span class="badge purple">{{$for.item.device}}</span>
762
- <span class="badge optimal" ~if="$for.item.weightsDownloaded">Веса на диске ({{$for.item.downloadedFiles?.length || 1}})</span>
763
- <span class="badge warn" ~if="$for.item.activeDownload">Загрузка {{$for.item.activeDownload.progress}}% ({{$for.item.activeDownload.speedMBs}} МБ/с)</span>
764
- </div>
765
- <div class="toolbar">
766
- <span class="badge">~{{$for.item.ramGb}} ГБ · ctx {{$for.item.maxTokens}}</span>
767
- <button type="button" class="btn" ~if="!$for.item.activeDownload" @tap="downloadHalogenModel($for.item.nodeName)">Скачать</button>
768
- </div>
769
- </div>
770
- <div class="progress-track" ~if="$for.item.activeDownload">
771
- <div class="progress-fill" ~style="{width: ($for.item.activeDownload.progress || 0) + '%'}"></div>
481
+ <div class="section-title" ~if="datasets.length" style="margin-top:8px;">Готовые наборы ({{datasets.length}})</div>
482
+ <div class="model-row" ~for="datasets">
483
+ <div class="model-main">
484
+ <div>
485
+ <div class="model-name">{{$for.item.name}}</div>
486
+ <div class="model-desc">Примеров: {{$for.item.totalExamples}} · ~{{$for.item.estimatedTokens}} токенов</div>
772
487
  </div>
773
- <div class="model-what">{{$for.item.role}}</div>
774
- <div style="font-size:11.5px;opacity:0.85;">Узел WORK: <code>{{$for.item.path}}</code> · Hugging Face: <code>{{$for.item.hfRepo}}</code></div>
488
+ <button type="button" class="btn" @tap="trainDatasetName = $for.item.name">Выбрать</button>
775
489
  </div>
776
490
  </div>
777
491
  </div>
778
492
 
779
- <div class="panel-right">
780
- <div class="section-title" ~if="downloads.length">Очередь загрузки пакета ({{downloads.length}})</div>
781
- <div class="card" ~for="downloads">
782
- <div class="model-top">
783
- <strong>{{$for.item.repoId}}</strong>
784
- <span class="badge purple">{{$for.item.status}} · {{$for.item.progress}}%</span>
785
- </div>
786
- <div style="font-size:11.5px;">Файл: {{$for.item.currentFile}} · Скорость: {{$for.item.speedMBs}} МБ/с · Путь: <code>{{$for.item.modelPath}}</code></div>
787
- <div class="progress-track">
788
- <div class="progress-fill" ~style="{width: ($for.item.progress || 0) + '%'}"></div>
789
- </div>
790
- <div class="error" ~if="$for.item.error">{{$for.item.error}}</div>
791
- <div class="toolbar" ~if="$for.item.status === 'downloading'">
792
- <button type="button" class="btn danger" @tap="cancelDownload($for.item.id)">Отменить</button>
793
- </div>
493
+ <!-- Шаг 2: Запуск дообучения LoRA -->
494
+ <div class="card">
495
+ <div class="section-title">Шаг 2. Дообучить модель (LoRA)</div>
496
+ <div class="field">
497
+ <label>Название новой обученной модели</label>
498
+ <input :value="trainAdapterName" @input="trainAdapterName = $this.value">
794
499
  </div>
795
-
796
- <div class="section-title">Нативный стек Windows 11 (Halowin Bubo / Gufo / Strata) и Linux Container</div>
797
- <div class="card" ~if="halogenState?.memoryPlan">
798
- <div style="font-size:11.5px;padding:6px 8px;border-radius:4px;background:rgba(234,179,8,0.12);border:1px solid rgba(234,179,8,0.35);">
799
- <strong>Важно про WSL2 и BIOS:</strong> {{halogenState.memoryPlan.wslWarning}}<br>
800
- <strong>Настройка памяти:</strong> {{halogenState.memoryPlan.biosRecommendation}}
801
- </div>
802
- <div style="font-size:12px;font-weight:700;margin-top:6px;">1. Windows 11 Native — Halowin Bubo (AMD ROCm 10.0.0 gfx1151 + Ryzen AI XRT NPU + MTP + Dense Twin):</div>
803
- <pre class="mono" style="background:rgba(128,128,128,0.08);padding:8px;border-radius:4px;margin:2px 0;">{{halogenState.memoryPlan.windowsPrepareCommand}}
804
- {{halogenState.memoryPlan.windowsCommand}}</pre>
805
- <div style="font-size:12px;font-weight:700;margin-top:6px;">2. Windows 11 — Мультимодальные сервисы Gufo (ASR / TTS / Embeddings / Image / SystemOne):</div>
806
- <pre class="mono" style="background:rgba(128,128,128,0.08);padding:8px;border-radius:4px;margin:2px 0;">{{halogenState.memoryPlan.windowsGufoServicesCommand}}</pre>
807
- <div style="font-size:12px;font-weight:700;margin-top:6px;">3. Windows 11 PowerShell (Администратор) — Фиксированный Page File 128 ГБ (131072 МБ):</div>
808
- <pre class="mono" style="background:rgba(128,128,128,0.08);padding:8px;border-radius:4px;margin:2px 0;">{{halogenState.memoryPlan.windowsPageFileCommand}}</pre>
809
- <div style="font-size:12px;font-weight:700;margin-top:6px;">4. Linux Native (halogen-flash-server ROCm /dev/kfd + NPU /dev/accel/accel0):</div>
810
- <pre class="mono" style="background:rgba(128,128,128,0.08);padding:8px;border-radius:4px;margin:2px 0;">{{halogenState.memoryPlan.linuxCommand}}</pre>
500
+ <div class="field">
501
+ <label>Базовая модель (рекомендована под вашу память: {{hardware?.recommendedBaseModel || 'Qwen/Qwen3-8B'}})</label>
502
+ <input :value="trainBaseModel" @input="trainBaseModel = $this.value">
811
503
  </div>
812
-
813
- <div class="section-title">Снимки KV-сессий субагентов (X-Halogen-Keep / X-Halogen-From)</div>
814
- <div class="card" ~if="!halogenHandles.length">
815
- <div style="font-size:12px;opacity:0.8;">Активных снимков KV-сессий пока нет. Они создаются автоматически при работе задачи <code>.task</code> и переиспользуются субагентами.</div>
504
+ <div class="field">
505
+ <label>Обучающий набор (.jsonl)</label>
506
+ <input :value="trainDatasetName" @input="trainDatasetName = $this.value">
507
+ </div>
508
+ <div class="field">
509
+ <label>Режим обучения</label>
510
+ <select @change="trainMode = $this.value">
511
+ <option value="sft" :selected="trainMode === 'sft'">Обучение на примерах организации (SFT)</option>
512
+ <option value="grpo" :selected="trainMode === 'grpo'">Обучение с проверкой учётных проводок (GRPO)</option>
513
+ </select>
816
514
  </div>
817
- <div class="card" ~for="halogenHandles">
818
- <div class="model-top">
819
- <strong>{{$for.item.name}}</strong>
820
- <span class="badge purple">Токенов: {{$for.item.tokens}} · Попаданий: {{$for.item.hits || 0}}</span>
515
+ <div class="toolbar" style="margin-top:4px;">
516
+ <button type="button" class="btn primary" @tap="startTraining()">Запустить дообучение</button>
517
+ </div>
518
+
519
+ <div class="section-title" ~if="jobs.length" style="margin-top:8px;">Задачи обучения ({{jobs.length}})</div>
520
+ <div class="model-row" ~for="jobs">
521
+ <div class="model-main">
522
+ <div>
523
+ <div class="model-name">{{$for.item.adapterName}} ({{$for.item.mode?.toUpperCase()}})</div>
524
+ <div class="model-desc">
525
+ Шаг {{$for.item.step}} / {{$for.item.totalSteps}} · Loss: {{$for.item.loss}}
526
+ <span ~if="$for.item.publishedPath"> · Готова: <code>{{$for.item.publishedPath}}</code></span>
527
+ </div>
528
+ </div>
529
+ <button type="button" class="btn danger" ~if="$for.item.status === 'running'" @tap="stopTraining($for.item.id)">Остановить</button>
530
+ </div>
531
+ <div class="progress-track">
532
+ <div class="progress-fill" ~style="{width: Math.round(($for.item.step / ($for.item.totalSteps || 1)) * 100) + '%'}"></div>
821
533
  </div>
822
534
  </div>
823
535
  </div>
824
536
  </div>
825
537
  `,
826
- activeTab: 'hf',
827
- halogenState: null,
828
- get halogenCatalog() {
829
- return this.halogenState?.catalog || [];
830
- },
831
- get halogenHandles() {
832
- return this.halogenState?.handles || [];
833
- },
834
- modalityTabs: MODALITY_TABS,
835
- capabilityOptions: CAPABILITY_OPTIONS,
538
+ activeTab: 'models',
836
539
  hardware: null,
540
+ runtime: null,
837
541
  localModels: [],
838
542
  adapters: [],
839
543
  datasets: [],
840
544
  downloads: [],
841
545
  jobs: [],
546
+ hfModels: [],
547
+ halogenCatalog: [],
842
548
  hfQuery: '',
843
549
  hfModality: 'all',
844
- hfCapability: '',
845
550
  hfFilter: 'all',
846
- hfToken: '',
847
- hfModels: [],
848
- selectedRepo: null,
849
- selectedQuant: 'Q8_0',
850
- datasetName: 'work-sft-dataset',
551
+ datasetName: 'work-org-sft',
851
552
  datasetBasePath: '/ORGANIZATIONS/Маслоторг',
852
553
  srcTasks: true,
853
554
  srcAccounting: true,
854
555
  srcClasses: true,
855
- selectedDataset: null,
856
- trainAdapterName: 'Маслоторг-Ассистент-LoRA',
857
- trainBaseModel: '',
858
- trainDatasetName: 'work-sft-dataset',
859
- trainMode: 'grpo',
556
+ trainAdapterName: 'work-accountant-lora',
557
+ trainBaseModel: 'Qwen/Qwen3-8B',
558
+ trainDatasetName: 'work-org-sft.jsonl',
559
+ trainMode: 'sft',
860
560
  trainLoraR: 16,
861
561
  trainMaxSeq: 8192,
862
- _defaultsApplied: false,
863
- verifySample: JSON.stringify({
864
- cid: '/ORGANIZATIONS/Маслоторг/MODULES/Register/OPERATIONS/Поступления на расчётный счёт/От покупателя',
865
- body: { sum: 50000, purpose: 'Оплата по договору поставки' },
866
- }, null, 2),
867
- verifyResult: null,
562
+ notice: '',
868
563
  error: '',
869
564
  _pollTimer: null,
870
- get activeJob() {
871
- return (this.jobs || [])[0] || null;
565
+
566
+ get categoryTabs() {
567
+ return [
568
+ { id: 'all', label: 'Все', icon: 'icons:apps' },
569
+ { id: 'llm', label: 'Чат и Код', icon: 'carbon:chat' },
570
+ { id: 'vlm', label: 'Зрение и Документы', icon: 'carbon:image-search' },
571
+ { id: 'audio', label: 'Речь и Голос', icon: 'carbon:microphone' },
572
+ { id: 'image', label: 'Картинки', icon: 'carbon:paint-brush' },
573
+ { id: 'embed', label: 'Поиск (RAG)', icon: 'carbon:search-locate' },
574
+ { id: 'halo', label: 'Комплект Halo (128–192 ГБ)', icon: 'carbon:chip' },
575
+ ];
576
+ },
577
+ get activeDownloads() {
578
+ return (this.downloads || []).filter(d => d.status === 'downloading');
579
+ },
580
+ isRepoInstalled(repoId) {
581
+ if (!repoId)
582
+ return false;
583
+ const shortName = String(repoId).split('/').pop().toLowerCase();
584
+ return (this.localModels || []).some(m =>
585
+ String(m.id || '').toLowerCase() === shortName
586
+ || String(m.model || '').toLowerCase() === String(repoId).toLowerCase()
587
+ );
872
588
  },
873
- get lossPolyline() {
874
- const hist = this.activeJob?.history || [];
875
- if (!hist.length)
876
- return '';
877
- const maxLoss = Math.max(...hist.map(h => Number(h.loss) || 0), 2.2);
878
- const minLoss = Math.min(...hist.map(h => Number(h.loss) || 0), 0.2);
879
- const span = Math.max(0.1, maxLoss - minLoss);
880
- return hist.map((h, idx) => {
881
- const x = Math.round((idx / Math.max(1, hist.length - 1)) * 304) + 8;
882
- const y = Math.round(98 - ((Number(h.loss) - minLoss) / span) * 84);
883
- return `${x},${y}`;
884
- }).join(' ');
589
+ getActiveDownload(repoId) {
590
+ if (!repoId)
591
+ return null;
592
+ return (this.downloads || []).find(d => d.status === 'downloading' && d.repoId === repoId) || null;
885
593
  },
886
594
  async attached() {
887
- const p = String(this.$item?.path || this.$item?.short || '');
888
- if (p === '/MODELS/Halogen' || p.startsWith('/MODELS/Halogen/'))
889
- this.activeTab = 'halogen';
890
- await this.reloadStatus();
595
+ await this.reloadAll();
891
596
  await this.searchHf();
892
- if (this.activeTab === 'halogen')
893
- await this.refreshHalogen();
597
+ this._pollTimer = setInterval(() => {
598
+ if (this.activeDownloads.length || (this.jobs || []).some(j => j.status === 'running')) {
599
+ this.reloadAll();
600
+ }
601
+ }, 1200);
894
602
  },
895
603
  detached() {
896
604
  if (this._pollTimer) {
@@ -898,53 +606,16 @@ export default {
898
606
  this._pollTimer = null;
899
607
  }
900
608
  },
901
- _ensurePolling() {
902
- const hasActive = (this.downloads || []).some(d => d.status === 'downloading')
903
- || (this.jobs || []).some(j => j.status === 'running');
904
- if (hasActive && !this._pollTimer) {
905
- this._pollTimer = setInterval(() => this.reloadStatus(), 500);
906
- }
907
- else if (!hasActive && this._pollTimer) {
908
- clearInterval(this._pollTimer);
909
- this._pollTimer = null;
910
- }
911
- },
912
- async reloadStatus() {
609
+ async _call(method, params = {}) {
610
+ this.error = '';
913
611
  if (!this.$item)
914
- return;
612
+ return null;
915
613
  try {
916
- const st = await this.$item.fetch('trainer_status');
917
- this.hardware = st?.hardware || null;
918
- if (this.hardware && !this._defaultsApplied) {
919
- this._defaultsApplied = true;
920
- this.trainBaseModel = this.hardware.recommendedBaseModel || 'Qwen/Qwen3-14B';
921
- this.trainLoraR = this.hardware.recommendedLoraR || 16;
922
- this.trainMaxSeq = this.hardware.recommendedMaxSeq || 8192;
923
- }
924
- const prevCount = this.localModels?.length || 0;
925
- this.localModels = Array.isArray(st?.models) ? st.models : [];
926
- if (this.localModels.length !== prevCount)
927
- this._invalidateClientModelsTree();
928
- this.adapters = Array.isArray(st?.adapters) ? st.adapters : [];
929
- this.datasets = Array.isArray(st?.datasets) ? st.datasets : [];
930
- this.downloads = Array.isArray(st?.downloads) ? st.downloads : [];
931
- this.jobs = Array.isArray(st?.jobs) ? st.jobs : [];
932
- if (st?.halogen) {
933
- this.halogenState = this.halogenState
934
- ? {
935
- ...this.halogenState,
936
- catalog: st.halogen.catalog,
937
- handles: st.halogen.handles,
938
- windowsEnv: st.halogen.windowsEnv || this.halogenState.windowsEnv,
939
- }
940
- : st.halogen;
941
- }
942
- if (!this.selectedDataset && this.datasets.length)
943
- this.selectedDataset = this.datasets[0];
944
- this._ensurePolling();
614
+ return await this.$item.fetch(method, params);
945
615
  }
946
616
  catch (e) {
947
617
  this.error = String(e?.message || e);
618
+ return null;
948
619
  }
949
620
  },
950
621
  _invalidateClientModelsTree() {
@@ -953,90 +624,125 @@ export default {
953
624
  const wk = w?.WORK;
954
625
  if (!wk?.items)
955
626
  continue;
956
- for (const p of ['/MODELS', '/MODELS/Local', '/MODELS/Halogen']) {
957
- const node = wk.items[p];
958
- if (node?.[w.R]?.cache)
959
- delete node[w.R].cache.items;
960
- node?.reset?.();
627
+ for (const k of Object.keys(wk.items)) {
628
+ if (k === '/MODELS' || k.startsWith('/MODELS/')) {
629
+ const node = wk.items[k];
630
+ if (node?.[w.R]?.cache)
631
+ delete node[w.R].cache.items;
632
+ node?.reset?.();
633
+ }
961
634
  }
962
635
  }
963
636
  catch { /* ignore cross-frame */ }
964
637
  }
965
638
  },
639
+ async reloadAll() {
640
+ const prevCount = this.localModels?.length || 0;
641
+ const st = await this._call('trainer_status');
642
+ if (!st)
643
+ return;
644
+ if (st.hardware) {
645
+ const firstLoad = !this.hardware;
646
+ this.hardware = st.hardware;
647
+ if (firstLoad && st.hardware.recommendedBaseModel) {
648
+ this.trainBaseModel = st.hardware.recommendedBaseModel;
649
+ this.trainLoraR = st.hardware.recommendedLoraR || 16;
650
+ this.trainMaxSeq = st.hardware.recommendedMaxSeq || 8192;
651
+ }
652
+ }
653
+ this.runtime = st.runtime || null;
654
+ this.localModels = st.models || [];
655
+ this.adapters = st.adapters || [];
656
+ this.datasets = st.datasets || [];
657
+ this.downloads = st.downloads || [];
658
+ this.jobs = st.jobs || [];
659
+ if (st.halogen?.catalog)
660
+ this.halogenCatalog = st.halogen.catalog;
661
+ if (this.localModels.length !== prevCount)
662
+ this._invalidateClientModelsTree();
663
+ },
966
664
  async setModality(mod) {
967
665
  this.hfModality = String(mod || 'all');
968
- await this.searchHf();
969
- },
970
- async setCapability(cap) {
971
- this.hfCapability = String(cap || '');
972
- await this.searchHf();
666
+ if (this.hfModality !== 'halo') {
667
+ await this.searchHf();
668
+ }
973
669
  },
974
670
  async setHfFilter(val) {
975
671
  this.hfFilter = String(val || 'all');
976
672
  await this.searchHf();
977
673
  },
978
674
  async searchHf() {
979
- this.error = '';
980
- if (!this.$item)
675
+ const res = await this._call('trainer_hf_search', {
676
+ query: this.hfQuery,
677
+ modality: this.hfModality === 'halo' ? 'all' : this.hfModality,
678
+ filter: this.hfFilter,
679
+ });
680
+ if (res?.items)
681
+ this.hfModels = res.items;
682
+ },
683
+ async installModelOneClick(repoId) {
684
+ if (!repoId)
981
685
  return;
982
- try {
983
- const res = await this.$item.fetch('trainer_hf_search', {
984
- query: this.hfQuery,
985
- modality: this.hfModality,
986
- capability: this.hfCapability,
987
- filter: this.hfFilter,
988
- token: this.hfToken,
989
- });
990
- this.hfModels = Array.isArray(res?.items) ? res.items : [];
991
- if (this.hfModels.length && (!this.selectedRepo || !this.hfModels.some(m => m.id === this.selectedRepo.id)))
992
- await this.inspectRepo(this.hfModels[0].id);
993
- }
994
- catch (e) {
995
- this.error = String(e?.message || e);
686
+ this.notice = '';
687
+ const quant = (this.hardware?.usableMemoryGb || 12) >= 24 ? 'Q8_0' : 'Q4_K_M';
688
+ const job = await this._call('trainer_hf_download', {
689
+ repoId,
690
+ quant,
691
+ });
692
+ if (job) {
693
+ this.notice = `Начата установка модели «${repoId}»…`;
694
+ await this.reloadAll();
695
+ this._invalidateClientModelsTree();
996
696
  }
997
697
  },
998
- async inspectRepo(repoId) {
999
- this.error = '';
1000
- if (!this.$item || !repoId)
698
+ async cancelDownload(jobId) {
699
+ await this._call('trainer_hf_cancel', { jobId });
700
+ await this.reloadAll();
701
+ },
702
+ async toggleLocalModel(modelPath, checked) {
703
+ await this._call('provider_toggle_model', {
704
+ nodePath: modelPath,
705
+ enabled: checked,
706
+ });
707
+ await this.reloadAll();
708
+ this._invalidateClientModelsTree();
709
+ },
710
+ async deleteLocalModel(modelId) {
711
+ if (!modelId)
1001
712
  return;
1002
- try {
1003
- this.selectedRepo = await this.$item.fetch('trainer_hf_inspect', {
1004
- repoId,
1005
- token: this.hfToken,
1006
- });
1007
- if (this.selectedRepo?.quants?.length)
1008
- this.selectedQuant = this.selectedRepo.quants[0];
713
+ const res = await this._call('trainer_delete_model', { modelId });
714
+ if (res?.ok) {
715
+ this.notice = `Модель «${modelId}» и её веса удалены с диска.`;
716
+ await this.reloadAll();
717
+ this._invalidateClientModelsTree();
1009
718
  }
1010
- catch (e) {
1011
- this.error = String(e?.message || e);
719
+ },
720
+ async releaseMemory() {
721
+ const res = await this._call('trainer_release_memory');
722
+ if (res?.ok) {
723
+ this.runtime = res.runtime || this.runtime;
724
+ this.notice = 'Оперативная память и видеопамять локального ИИ освобождены.';
1012
725
  }
1013
726
  },
1014
- async downloadRepo(repoId) {
1015
- this.error = '';
1016
- if (!this.$item || !repoId)
727
+ async downloadHalogenModel(nodeName) {
728
+ if (!nodeName)
1017
729
  return;
1018
- try {
1019
- await this.$item.fetch('trainer_hf_download', {
1020
- repoId,
1021
- quant: this.selectedQuant || 'Q8_0',
1022
- token: this.hfToken,
1023
- });
1024
- await this.reloadStatus();
1025
- }
1026
- catch (e) {
1027
- this.error = String(e?.message || e);
730
+ const res = await this._call('halogen_download_bundle', { models: nodeName });
731
+ if (res) {
732
+ this.notice = `Начата установка модели «${nodeName}» в /MODELS/Local…`;
733
+ await this.reloadAll();
734
+ this._invalidateClientModelsTree();
1028
735
  }
1029
736
  },
1030
- async cancelDownload(jobId) {
1031
- if (!this.$item || !jobId)
1032
- return;
1033
- await this.$item.fetch('trainer_hf_cancel', { jobId });
1034
- await this.reloadStatus();
737
+ async downloadHalogenBundleAll() {
738
+ const res = await this._call('halogen_download_bundle', {});
739
+ if (res) {
740
+ this.notice = `Начата установка комплекта Halo (${res.totalModels} моделей GPU + NPU) в /MODELS/Local…`;
741
+ await this.reloadAll();
742
+ this._invalidateClientModelsTree();
743
+ }
1035
744
  },
1036
745
  async buildDataset() {
1037
- this.error = '';
1038
- if (!this.$item)
1039
- return;
1040
746
  const sources = [];
1041
747
  if (this.srcTasks)
1042
748
  sources.push('tasks');
@@ -1044,130 +750,34 @@ export default {
1044
750
  sources.push('accounting');
1045
751
  if (this.srcClasses)
1046
752
  sources.push('classes');
1047
- try {
1048
- const meta = await this.$item.fetch('trainer_dataset_build', {
1049
- name: this.datasetName,
1050
- basePath: this.datasetBasePath,
1051
- sources: sources.join(','),
1052
- });
1053
- this.selectedDataset = meta;
1054
- this.trainDatasetName = meta?.name || this.datasetName;
1055
- await this.reloadStatus();
1056
- }
1057
- catch (e) {
1058
- this.error = String(e?.message || e);
753
+ const ds = await this._call('trainer_dataset_build', {
754
+ name: this.datasetName,
755
+ basePath: this.datasetBasePath,
756
+ sources: sources.join(','),
757
+ });
758
+ if (ds) {
759
+ this.trainDatasetName = `${ds.name}.jsonl`;
760
+ this.notice = `Обучающий набор «${ds.name}» собран (${ds.totalExamples} примеров).`;
761
+ await this.reloadAll();
1059
762
  }
1060
763
  },
1061
764
  async startTraining() {
1062
- this.error = '';
1063
- if (!this.$item)
1064
- return;
1065
- try {
1066
- await this.$item.fetch('trainer_start', {
1067
- adapterName: this.trainAdapterName,
1068
- baseModel: this.trainBaseModel,
1069
- datasetName: this.trainDatasetName,
1070
- basePath: this.datasetBasePath,
1071
- mode: this.trainMode,
1072
- loraR: this.trainLoraR,
1073
- maxSeqLength: this.trainMaxSeq,
1074
- });
1075
- await this.reloadStatus();
1076
- }
1077
- catch (e) {
1078
- this.error = String(e?.message || e);
765
+ const job = await this._call('trainer_start', {
766
+ adapterName: this.trainAdapterName,
767
+ baseModel: this.trainBaseModel,
768
+ datasetName: this.trainDatasetName,
769
+ mode: this.trainMode,
770
+ loraR: this.trainLoraR,
771
+ maxSeqLength: this.trainMaxSeq,
772
+ basePath: this.datasetBasePath,
773
+ });
774
+ if (job) {
775
+ this.notice = `Запущено дообучение модели «${this.trainAdapterName}»…`;
776
+ await this.reloadAll();
1079
777
  }
1080
778
  },
1081
779
  async stopTraining(jobId) {
1082
- if (!this.$item || !jobId)
1083
- return;
1084
- await this.$item.fetch('trainer_stop', { jobId });
1085
- await this.reloadStatus();
1086
- },
1087
- async runVerify() {
1088
- this.error = '';
1089
- if (!this.$item)
1090
- return;
1091
- try {
1092
- this.verifyResult = await this.$item.fetch('trainer_verify', {
1093
- basePath: this.datasetBasePath,
1094
- taskType: 'operation',
1095
- completion: this.verifySample,
1096
- });
1097
- }
1098
- catch (e) {
1099
- this.error = String(e?.message || e);
1100
- }
1101
- },
1102
- async openHalogenTab() {
1103
- this.activeTab = 'halogen';
1104
- await this.refreshHalogen();
1105
- },
1106
- async refreshHalogen() {
1107
- if (!this.$item)
1108
- return;
1109
- try {
1110
- this.halogenState = await this.$item.fetch('halogen_status');
1111
- }
1112
- catch (e) {
1113
- this.error = String(e?.message || e);
1114
- }
1115
- },
1116
- async startHalogenServer() {
1117
- this.error = '';
1118
- if (!this.$item)
1119
- return;
1120
- try {
1121
- await this.$item.fetch('halogen_start');
1122
- await this.refreshHalogen();
1123
- }
1124
- catch (e) {
1125
- this.error = String(e?.message || e);
1126
- }
1127
- },
1128
- async prepareWindowsBubo(step = 'runtime') {
1129
- this.error = '';
1130
- if (!this.$item)
1131
- return;
1132
- try {
1133
- const res = await this.$item.fetch('halogen_prepare_windows', { step });
1134
- if (res?.error)
1135
- this.error = res.error;
1136
- await this.refreshHalogen();
1137
- }
1138
- catch (e) {
1139
- this.error = String(e?.message || e);
1140
- }
1141
- },
1142
- async downloadHalogenBundleAll() {
1143
- this.error = '';
1144
- if (!this.$item)
1145
- return;
1146
- try {
1147
- await this.$item.fetch('halogen_download_bundle', {
1148
- token: this.hfToken,
1149
- });
1150
- await this.reloadStatus();
1151
- await this.refreshHalogen();
1152
- }
1153
- catch (e) {
1154
- this.error = String(e?.message || e);
1155
- }
1156
- },
1157
- async downloadHalogenModel(nodeName) {
1158
- this.error = '';
1159
- if (!this.$item || !nodeName)
1160
- return;
1161
- try {
1162
- await this.$item.fetch('halogen_download_bundle', {
1163
- models: nodeName,
1164
- token: this.hfToken,
1165
- });
1166
- await this.reloadStatus();
1167
- await this.refreshHalogen();
1168
- }
1169
- catch (e) {
1170
- this.error = String(e?.message || e);
1171
- }
780
+ await this._call('trainer_stop', { jobId });
781
+ await this.reloadAll();
1172
782
  },
1173
- };
783
+ }