odant-work 1.0.16 → 1.0.18
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/#system/package-manifest.json +16 -16
- package/$server/$folder/handlers/pages/form/$handler/form.js +2 -1
- package/MODELS/$ai/$folder/$class/$ai/class.js +1 -1
- package/MODELS/$ai/$folder/$class/$ai/handlers/pages/form/trainer/$handler/class.js +6 -4
- package/MODELS/$ai/$folder/$class/$ai/handlers/pages/form/trainer/$handler/trainer.js +344 -3
- package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/provider/$handler/class.js +3 -3
- package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/class.js +3 -3
- package/MODELS/$ai/$folder/$class/$provider/handlers/pages/form/trainer/$handler/trainer.js +530 -920
- package/package.json +1 -1
- package/sources/data/progress.md +18 -0
- package/sources/modules/trainer/index.js +41 -1
- package/sources/modules/trainer/local-engine.js +67 -1
- package/sources/modules/trainer/runner.js +47 -1
|
@@ -1,35 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* form/trainer
|
|
3
|
-
*
|
|
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:
|
|
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:
|
|
52
|
-
padding:
|
|
53
|
-
|
|
54
|
-
border
|
|
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
|
-
.
|
|
58
|
-
@apply --
|
|
59
|
-
|
|
33
|
+
.header-left {
|
|
34
|
+
@apply --horizontal;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 12px;
|
|
60
37
|
min-width: 0;
|
|
61
38
|
}
|
|
62
|
-
.
|
|
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-
|
|
68
|
-
@apply --horizontal;
|
|
69
|
-
gap: 6px;
|
|
70
|
-
align-items: center;
|
|
71
|
-
flex-wrap: wrap;
|
|
43
|
+
.hw-line {
|
|
72
44
|
font-size: 12px;
|
|
73
|
-
|
|
45
|
+
opacity: 0.8;
|
|
46
|
+
margin-top: 2px;
|
|
74
47
|
}
|
|
75
|
-
.
|
|
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:
|
|
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:
|
|
96
|
-
padding:
|
|
57
|
+
gap: 6px;
|
|
58
|
+
padding: 3px 10px;
|
|
97
59
|
border-radius: 999px;
|
|
98
|
-
font-size:
|
|
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.
|
|
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.
|
|
118
|
-
background: light-dark(#
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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:
|
|
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
|
-
|
|
104
|
+
opacity: 1;
|
|
168
105
|
}
|
|
169
106
|
.tab-btn.tab-active {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
color:
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
122
|
+
font-size: 12px;
|
|
258
123
|
text-transform: uppercase;
|
|
259
124
|
letter-spacing: 0.04em;
|
|
260
|
-
|
|
261
|
-
border-bottom: 1px solid
|
|
262
|
-
padding-bottom:
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
.
|
|
296
|
-
background:
|
|
297
|
-
border-color: #
|
|
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
|
-
/*
|
|
307
|
-
.
|
|
150
|
+
/* Строка модели (установленной или в каталоге) */
|
|
151
|
+
.model-row {
|
|
308
152
|
@apply --vertical;
|
|
309
|
-
flex-shrink: 0;
|
|
310
153
|
gap: 6px;
|
|
311
|
-
padding:
|
|
312
|
-
border: 1px solid light-dark(#d6d5cc, #343642);
|
|
154
|
+
padding: 10px 12px;
|
|
313
155
|
border-radius: 6px;
|
|
314
|
-
|
|
315
|
-
|
|
156
|
+
border: 1px solid var(--border-color, #e2e8f0);
|
|
157
|
+
background: var(--subtle-background, #f8fafc);
|
|
316
158
|
}
|
|
317
|
-
.
|
|
318
|
-
|
|
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-
|
|
162
|
+
.model-main {
|
|
329
163
|
@apply --horizontal;
|
|
330
|
-
justify-content: space-between;
|
|
331
164
|
align-items: center;
|
|
332
|
-
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
gap: 12px;
|
|
333
167
|
flex-wrap: wrap;
|
|
334
168
|
}
|
|
335
|
-
.model-
|
|
169
|
+
.model-left {
|
|
336
170
|
@apply --horizontal;
|
|
337
171
|
align-items: center;
|
|
338
|
-
gap:
|
|
339
|
-
flex-wrap: wrap;
|
|
172
|
+
gap: 10px;
|
|
340
173
|
min-width: 0;
|
|
174
|
+
flex: 1;
|
|
341
175
|
}
|
|
342
|
-
.model-
|
|
176
|
+
.model-name {
|
|
343
177
|
font-weight: 700;
|
|
344
178
|
font-size: 13.5px;
|
|
345
|
-
color: light-dark(#0f172a, #f8fafc);
|
|
346
179
|
}
|
|
347
|
-
.model-
|
|
180
|
+
.model-desc {
|
|
348
181
|
font-size: 12px;
|
|
349
|
-
|
|
350
|
-
|
|
182
|
+
opacity: 0.8;
|
|
183
|
+
margin-top: 2px;
|
|
351
184
|
}
|
|
352
|
-
.
|
|
353
|
-
|
|
185
|
+
.mem-tag {
|
|
186
|
+
display: inline-flex;
|
|
354
187
|
align-items: center;
|
|
355
188
|
gap: 5px;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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:
|
|
367
|
-
background:
|
|
368
|
-
border-radius:
|
|
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:
|
|
204
|
+
background: var(--accent-color, #2563eb);
|
|
374
205
|
transition: width 0.2s ease;
|
|
375
206
|
}
|
|
376
|
-
|
|
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(
|
|
379
|
-
gap:
|
|
219
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
220
|
+
gap: 14px;
|
|
380
221
|
}
|
|
381
|
-
.field
|
|
222
|
+
.field {
|
|
382
223
|
@apply --vertical;
|
|
383
|
-
gap:
|
|
224
|
+
gap: 4px;
|
|
384
225
|
}
|
|
385
|
-
.field
|
|
386
|
-
font-size:
|
|
226
|
+
.field label {
|
|
227
|
+
font-size: 12px;
|
|
387
228
|
font-weight: 600;
|
|
388
|
-
color: light-dark(#475569, #94a3b8);
|
|
389
229
|
}
|
|
390
|
-
|
|
391
|
-
padding:
|
|
392
|
-
|
|
393
|
-
border
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
.
|
|
404
|
-
display: flex;
|
|
405
|
-
justify-content: space-between;
|
|
238
|
+
.btn {
|
|
239
|
+
display: inline-flex;
|
|
406
240
|
align-items: center;
|
|
407
|
-
gap:
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
.
|
|
412
|
-
|
|
413
|
-
|
|
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="
|
|
421
|
-
<div class="
|
|
422
|
-
<
|
|
423
|
-
<div
|
|
424
|
-
<
|
|
425
|
-
<
|
|
426
|
-
|
|
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="
|
|
430
|
-
<span class="badge
|
|
431
|
-
|
|
432
|
-
|
|
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 === '
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<button type="button" class="tab-btn" ~class="{'tab-active': activeTab === '
|
|
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
|
-
|
|
309
|
+
<!-- ВКЛАДКА 1: МОДЕЛИ (УСТАНОВЛЕННЫЕ + КАТАЛОГ) -->
|
|
310
|
+
<div ~if="activeTab === 'models'" class="vertical" style="gap:14px;">
|
|
444
311
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
<div
|
|
450
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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="
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
<div class="
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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="
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
606
|
-
<div
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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="
|
|
653
|
-
<
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
<div class="
|
|
670
|
-
<div class="model-
|
|
671
|
-
<
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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="
|
|
686
|
-
<
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
<
|
|
699
|
-
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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="
|
|
755
|
-
|
|
756
|
-
<div class="
|
|
757
|
-
<div
|
|
758
|
-
<div class="model-
|
|
759
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
<div class="
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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="
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
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
|
-
|
|
893
|
-
|
|
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
|
-
|
|
902
|
-
|
|
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
|
-
|
|
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
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
980
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
this.
|
|
991
|
-
|
|
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
|
|
999
|
-
this.
|
|
1000
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
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
|
|
1015
|
-
|
|
1016
|
-
if (!this.$item || !repoId)
|
|
727
|
+
async downloadHalogenModel(nodeName) {
|
|
728
|
+
if (!nodeName)
|
|
1017
729
|
return;
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
this.
|
|
1054
|
-
this.
|
|
1055
|
-
await this.
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
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
|
+
}
|