kingkont 0.11.2 → 0.11.4
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/package.json +1 -1
- package/renderer/board.js +82 -31
- package/renderer/chat.js +72 -12
- package/renderer/cloudProjects.js +2 -30
- package/renderer/styles.css +12 -7
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -588,6 +588,7 @@ async function renderWelcomeRecents() {
|
|
|
588
588
|
}
|
|
589
589
|
|
|
590
590
|
// Карточка локального (папочного) проекта — извлечена из renderWelcomeRecents.
|
|
591
|
+
// Удаление и save-as-template — через ПКМ (по аналогии с cloud-карточкой).
|
|
591
592
|
function makeRecentWelcomeCard(r) {
|
|
592
593
|
const card = document.createElement('div');
|
|
593
594
|
card.className = 'welcome-card';
|
|
@@ -609,17 +610,6 @@ function makeRecentWelcomeCard(r) {
|
|
|
609
610
|
tsEl.textContent = fmtRelativeTime(r.ts);
|
|
610
611
|
meta.append(nameEl, tsEl);
|
|
611
612
|
card.append(thumb, meta);
|
|
612
|
-
const del = document.createElement('div');
|
|
613
|
-
del.className = 'welcome-card-del';
|
|
614
|
-
del.textContent = '×';
|
|
615
|
-
del.title = 'Удалить из недавних';
|
|
616
|
-
del.addEventListener('click', async e => {
|
|
617
|
-
e.stopPropagation();
|
|
618
|
-
if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
|
|
619
|
-
await removeRecent(r.name);
|
|
620
|
-
await renderWelcomeRecents();
|
|
621
|
-
});
|
|
622
|
-
card.appendChild(del);
|
|
623
613
|
card.addEventListener('click', async () => {
|
|
624
614
|
try {
|
|
625
615
|
if (r.handle) {
|
|
@@ -642,9 +632,50 @@ function makeRecentWelcomeCard(r) {
|
|
|
642
632
|
alert('Ошибка: ' + (err?.message || err));
|
|
643
633
|
}
|
|
644
634
|
});
|
|
635
|
+
card.addEventListener('contextmenu', e => {
|
|
636
|
+
e.preventDefault();
|
|
637
|
+
e.stopPropagation();
|
|
638
|
+
showRecentCardContextMenu(r, e.clientX, e.clientY);
|
|
639
|
+
});
|
|
645
640
|
return card;
|
|
646
641
|
}
|
|
647
642
|
|
|
643
|
+
// ПКМ-меню для recent (folder) welcome-карточки. По аналогии с cloud:
|
|
644
|
+
// «Сохранить как шаблон» (открывает проект → saveCurrentProjectAsTemplate)
|
|
645
|
+
// + «Убрать из недавних».
|
|
646
|
+
function showRecentCardContextMenu(r, clientX, clientY) {
|
|
647
|
+
const menu = $('nodeMenu');
|
|
648
|
+
if (!menu) return;
|
|
649
|
+
menu.innerHTML = '';
|
|
650
|
+
const add = (label, fn, opts = {}) => {
|
|
651
|
+
const b = document.createElement('button');
|
|
652
|
+
b.textContent = label;
|
|
653
|
+
if (opts.danger) b.style.color = '#f88';
|
|
654
|
+
b.addEventListener('click', () => { menu.classList.add('hidden'); fn(); });
|
|
655
|
+
menu.appendChild(b);
|
|
656
|
+
};
|
|
657
|
+
add('💾 Сохранить как шаблон…', async () => {
|
|
658
|
+
if (!r.handle) { alert('Handle потерян, сначала открой проект и сохрани заново.'); return; }
|
|
659
|
+
try {
|
|
660
|
+
// Открываем сначала — saveCurrentProjectAsTemplate работает с filmHandle.
|
|
661
|
+
let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
|
|
662
|
+
if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
|
|
663
|
+
if (!g) { alert('Доступ к папке не подтверждён.'); return; }
|
|
664
|
+
await openFilm(r.handle);
|
|
665
|
+
if (typeof saveCurrentProjectAsTemplate === 'function') {
|
|
666
|
+
await saveCurrentProjectAsTemplate();
|
|
667
|
+
}
|
|
668
|
+
} catch (e) { alert('Ошибка: ' + (e?.message || e)); }
|
|
669
|
+
});
|
|
670
|
+
add('✖ Убрать из недавних', async () => {
|
|
671
|
+
if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
|
|
672
|
+
await removeRecent(r.name);
|
|
673
|
+
await renderWelcomeRecents();
|
|
674
|
+
}, { danger: true });
|
|
675
|
+
positionFloatingMenu(menu, clientX, clientY);
|
|
676
|
+
setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
|
|
677
|
+
}
|
|
678
|
+
|
|
648
679
|
// Карточка облачного проекта. Визуально такая же как у папочного, плюс
|
|
649
680
|
// ☁ бейдж в углу обложки. Клик → cloudProjects.open() (использует local
|
|
650
681
|
// cache если синхронизирован, иначе скачивает manifest+файлы).
|
|
@@ -715,6 +746,42 @@ function showCloudCardContextMenu(p, clientX, clientY) {
|
|
|
715
746
|
add('↻ Обновить из облака', () => {
|
|
716
747
|
if (window.cloudProjects?.open) window.cloudProjects.open(p.id, p.name, { forceRefresh: true });
|
|
717
748
|
});
|
|
749
|
+
add('💾 Сохранить как шаблон…', async () => {
|
|
750
|
+
// Берём cloud-проект с сервера (manifest+files) и постим в /api/templates
|
|
751
|
+
// в template-формате (board.scene → board.manifest). Без открытия проекта.
|
|
752
|
+
const tplName = await askName('Имя шаблона:', '', p.name || '', { okText: 'Сохранить' });
|
|
753
|
+
if (!tplName) return;
|
|
754
|
+
try {
|
|
755
|
+
const r = await fetch('/api/projects/' + encodeURIComponent(p.id));
|
|
756
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
757
|
+
const proj = await r.json();
|
|
758
|
+
const tplBoards = (proj.manifest?.boards || []).map(b => ({
|
|
759
|
+
kind: b.kind, name: b.name,
|
|
760
|
+
manifest: b.scene || {}, // board.scene в cloud → board.manifest в template
|
|
761
|
+
files: b.files || {},
|
|
762
|
+
}));
|
|
763
|
+
const allFiles = {};
|
|
764
|
+
for (const b of tplBoards) {
|
|
765
|
+
for (const [rel, url] of Object.entries(b.files || {})) allFiles[`${b.kind}/${b.name}/${rel}`] = url;
|
|
766
|
+
}
|
|
767
|
+
const tR = await fetch('/api/templates', {
|
|
768
|
+
method: 'POST',
|
|
769
|
+
headers: { 'Content-Type': 'application/json' },
|
|
770
|
+
body: JSON.stringify({
|
|
771
|
+
name: tplName, kind: 'project',
|
|
772
|
+
manifest: { boards: tplBoards, coverUrl: proj.coverUrl || null },
|
|
773
|
+
files: allFiles,
|
|
774
|
+
}),
|
|
775
|
+
});
|
|
776
|
+
if (!tR.ok) {
|
|
777
|
+
const err = await tR.json().catch(() => ({}));
|
|
778
|
+
throw new Error(err.error || 'HTTP ' + tR.status);
|
|
779
|
+
}
|
|
780
|
+
alert(`Шаблон «${tplName}» создан.`);
|
|
781
|
+
} catch (e) {
|
|
782
|
+
alert('Не удалось сохранить шаблон: ' + (e?.message || e));
|
|
783
|
+
}
|
|
784
|
+
});
|
|
718
785
|
add('🗑 Удалить с сервера', async () => {
|
|
719
786
|
if (!confirm(`Удалить «${p.name}» с сервера? Локальная копия в userData останется.`)) return;
|
|
720
787
|
try {
|
|
@@ -2315,27 +2382,11 @@ function showNodeContextMenu(node, clientX, clientY) {
|
|
|
2315
2382
|
setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
|
|
2316
2383
|
}
|
|
2317
2384
|
|
|
2318
|
-
// ПКМ на brand-area
|
|
2319
|
-
//
|
|
2320
|
-
//
|
|
2321
|
-
// определит mode по .kingkont-meta.json).
|
|
2385
|
+
// (ПКМ на brand-area внутри проекта раньше показывал «Сохранить как шаблон» —
|
|
2386
|
+
// убрано, переехало в ПКМ карточки на welcome-screen. Функция оставлена
|
|
2387
|
+
// для backward-compat если будут добавляться другие project-actions.)
|
|
2322
2388
|
function showProjectContextMenu(clientX, clientY) {
|
|
2323
|
-
|
|
2324
|
-
menu.innerHTML = '';
|
|
2325
|
-
const add = (label, fn) => {
|
|
2326
|
-
const b = document.createElement('button');
|
|
2327
|
-
b.textContent = label;
|
|
2328
|
-
b.addEventListener('click', () => {
|
|
2329
|
-
menu.classList.add('hidden');
|
|
2330
|
-
fn();
|
|
2331
|
-
});
|
|
2332
|
-
menu.appendChild(b);
|
|
2333
|
-
};
|
|
2334
|
-
add('💾 Сохранить как шаблон…', () => {
|
|
2335
|
-
if (typeof saveCurrentProjectAsTemplate === 'function') saveCurrentProjectAsTemplate();
|
|
2336
|
-
});
|
|
2337
|
-
positionFloatingMenu(menu, clientX, clientY);
|
|
2338
|
-
setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
|
|
2389
|
+
// intentional no-op: пока что внутри проекта ПКМ на brand ничего не делает.
|
|
2339
2390
|
}
|
|
2340
2391
|
|
|
2341
2392
|
// ПКМ на 📚 Templates-кнопке — действия проектного уровня.
|
package/renderer/chat.js
CHANGED
|
@@ -269,9 +269,13 @@
|
|
|
269
269
|
// <tool>JSON</tool> — наш command-protocol (модель так зовёт tools)
|
|
270
270
|
// <tool_result>JSON</tool_result> — модель иногда его галюцинирует
|
|
271
271
|
// в outputе, копируя format из user-msg.
|
|
272
|
+
// После стрипа нормализуем whitespace — иначе остаются «дыры» и
|
|
273
|
+
// подряд идущие space'ы (3+ → 1, blank lines 3+ → 2).
|
|
272
274
|
return text
|
|
273
275
|
.replace(/<tool>[\s\S]*?<\/tool>/g, '')
|
|
274
276
|
.replace(/<tool_result>[\s\S]*?<\/tool_result>/g, '')
|
|
277
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
278
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
275
279
|
.trim();
|
|
276
280
|
}
|
|
277
281
|
|
|
@@ -333,7 +337,19 @@
|
|
|
333
337
|
div.className = 'chat-msg chat-msg-' + m.role;
|
|
334
338
|
const lbl = document.createElement('div');
|
|
335
339
|
lbl.className = 'chat-msg-role';
|
|
336
|
-
|
|
340
|
+
if (m.role === 'assistant') {
|
|
341
|
+
const img = document.createElement('img');
|
|
342
|
+
img.src = 'assets/icon.png';
|
|
343
|
+
img.className = 'chat-msg-avatar';
|
|
344
|
+
img.draggable = false;
|
|
345
|
+
img.alt = '';
|
|
346
|
+
lbl.appendChild(img);
|
|
347
|
+
const span = document.createElement('span');
|
|
348
|
+
span.textContent = 'KingKont';
|
|
349
|
+
lbl.appendChild(span);
|
|
350
|
+
} else {
|
|
351
|
+
lbl.textContent = 'Вы';
|
|
352
|
+
}
|
|
337
353
|
div.appendChild(lbl);
|
|
338
354
|
const body = document.createElement('div');
|
|
339
355
|
body.className = 'chat-msg-body';
|
|
@@ -441,7 +457,7 @@
|
|
|
441
457
|
history.push({ role: 'user', content: userText });
|
|
442
458
|
renderHistory();
|
|
443
459
|
persistDebounced();
|
|
444
|
-
const status = appendStatus('
|
|
460
|
+
const status = appendStatus('KingKont думает…');
|
|
445
461
|
const system = buildSystemPrompt();
|
|
446
462
|
try {
|
|
447
463
|
let iter = 0;
|
|
@@ -518,26 +534,54 @@
|
|
|
518
534
|
for (const m of history) {
|
|
519
535
|
if (m.role === 'system') continue;
|
|
520
536
|
if (m.role === 'user' && m.content?.startsWith('<tool_result>')) continue; // системный turn
|
|
537
|
+
// Пустой assistant без tools — не рендерим (бывает при streaming/parse-fail).
|
|
538
|
+
const hasContent = !!(m.content && m.content.trim());
|
|
539
|
+
const hasTools = Array.isArray(m.tools) && m.tools.length;
|
|
540
|
+
if (!hasContent && !hasTools) continue;
|
|
521
541
|
const div = document.createElement('div');
|
|
522
542
|
div.className = 'chat-msg chat-msg-' + m.role;
|
|
523
543
|
const lbl = document.createElement('div');
|
|
524
544
|
lbl.className = 'chat-msg-role';
|
|
525
|
-
|
|
545
|
+
if (m.role === 'assistant') {
|
|
546
|
+
const img = document.createElement('img');
|
|
547
|
+
img.src = 'assets/icon.png';
|
|
548
|
+
img.className = 'chat-msg-avatar';
|
|
549
|
+
img.draggable = false;
|
|
550
|
+
img.alt = '';
|
|
551
|
+
lbl.appendChild(img);
|
|
552
|
+
const span = document.createElement('span');
|
|
553
|
+
span.textContent = 'KingKont';
|
|
554
|
+
lbl.appendChild(span);
|
|
555
|
+
} else {
|
|
556
|
+
lbl.textContent = 'Вы';
|
|
557
|
+
}
|
|
526
558
|
div.appendChild(lbl);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
559
|
+
if (hasContent) {
|
|
560
|
+
const body = document.createElement('div');
|
|
561
|
+
body.className = 'chat-msg-body';
|
|
562
|
+
body.textContent = m.content;
|
|
563
|
+
div.appendChild(body);
|
|
564
|
+
}
|
|
565
|
+
if (hasTools) {
|
|
532
566
|
for (const tc of m.tools) {
|
|
533
567
|
const t = document.createElement('details');
|
|
534
568
|
t.className = 'chat-tool';
|
|
535
569
|
const sum = document.createElement('summary');
|
|
536
|
-
|
|
570
|
+
// Компактный summary: имя tool + ключевой arg (если короткий) + статус.
|
|
571
|
+
const argHint = _argHint(tc.args);
|
|
572
|
+
const status = tc._error ? ' ⚠' : tc._ok ? ' ✓' : '';
|
|
573
|
+
sum.textContent = `🔧 ${tc.name}${argHint}${status}`;
|
|
537
574
|
t.appendChild(sum);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
575
|
+
// pre добавляем только если есть что показать (args/result/error не пусты).
|
|
576
|
+
const dump = {};
|
|
577
|
+
if (tc.args && Object.keys(tc.args).length) dump.args = tc.args;
|
|
578
|
+
if (tc.result !== undefined) dump.result = tc.result;
|
|
579
|
+
if (tc._error) dump.error = tc._error;
|
|
580
|
+
if (Object.keys(dump).length) {
|
|
581
|
+
const pre = document.createElement('pre');
|
|
582
|
+
pre.textContent = JSON.stringify(dump, null, 2);
|
|
583
|
+
t.appendChild(pre);
|
|
584
|
+
}
|
|
541
585
|
div.appendChild(t);
|
|
542
586
|
}
|
|
543
587
|
}
|
|
@@ -545,6 +589,22 @@
|
|
|
545
589
|
}
|
|
546
590
|
list.scrollTop = list.scrollHeight;
|
|
547
591
|
}
|
|
592
|
+
|
|
593
|
+
// Короткая подсказка про args для summary tool-блока.
|
|
594
|
+
// 'add_node({type:image, name:"..."})' — без полного JSON.
|
|
595
|
+
function _argHint(args) {
|
|
596
|
+
if (!args || typeof args !== 'object') return '';
|
|
597
|
+
const keys = Object.keys(args);
|
|
598
|
+
if (!keys.length) return '';
|
|
599
|
+
const items = [];
|
|
600
|
+
for (const k of keys) {
|
|
601
|
+
const v = args[k];
|
|
602
|
+
let s = typeof v === 'string' ? `"${v.length > 30 ? v.slice(0, 30) + '…' : v}"` : String(v);
|
|
603
|
+
items.push(`${k}:${s}`);
|
|
604
|
+
if (items.join(',').length > 60) break;
|
|
605
|
+
}
|
|
606
|
+
return ` (${items.join(', ')})`;
|
|
607
|
+
}
|
|
548
608
|
// Перебиндим
|
|
549
609
|
renderHistory = renderHistoryFiltered;
|
|
550
610
|
|
|
@@ -644,36 +644,8 @@
|
|
|
644
644
|
$('newCloudProject')?.addEventListener('click', createNewCloudProject);
|
|
645
645
|
$('openCloudProjects')?.addEventListener('click', openCloudProjectsModal);
|
|
646
646
|
$('saveProjectCloud')?.addEventListener('click', saveCloudProject);
|
|
647
|
-
// ПКМ на «☁ Сохранить на сервер» —
|
|
648
|
-
//
|
|
649
|
-
// в templates.js работает через filmHandle, cloudFs-shim тоже подходит).
|
|
650
|
-
$('saveProjectCloud')?.addEventListener('contextmenu', e => {
|
|
651
|
-
e.preventDefault();
|
|
652
|
-
e.stopPropagation();
|
|
653
|
-
const menu = document.getElementById('nodeMenu');
|
|
654
|
-
if (!menu) return;
|
|
655
|
-
menu.innerHTML = '';
|
|
656
|
-
const add = (label, fn) => {
|
|
657
|
-
const b = document.createElement('button');
|
|
658
|
-
b.textContent = label;
|
|
659
|
-
b.addEventListener('click', () => { menu.classList.add('hidden'); fn(); });
|
|
660
|
-
menu.appendChild(b);
|
|
661
|
-
};
|
|
662
|
-
add('☁ Сохранить на сервер', saveCloudProject);
|
|
663
|
-
add('💾 Сохранить как шаблон…', () => {
|
|
664
|
-
if (typeof saveCurrentProjectAsTemplate === 'function') saveCurrentProjectAsTemplate();
|
|
665
|
-
else alert('saveCurrentProjectAsTemplate недоступен');
|
|
666
|
-
});
|
|
667
|
-
if (typeof positionFloatingMenu === 'function') {
|
|
668
|
-
positionFloatingMenu(menu, e.clientX, e.clientY);
|
|
669
|
-
} else {
|
|
670
|
-
menu.style.cssText = `position:fixed; left:${e.clientX}px; top:${e.clientY}px; z-index:9999;`;
|
|
671
|
-
}
|
|
672
|
-
// Используем глобальный closeNodeMenu — он умеет НЕ закрываться на клик
|
|
673
|
-
// по активной кнопке внутри меню (даёт click-обработчику сработать).
|
|
674
|
-
// Без этого mousedown на кнопке закрывал меню до click → нажатие тонуло.
|
|
675
|
-
setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
|
|
676
|
-
});
|
|
647
|
+
// (ПКМ на «☁ Сохранить на сервер» убран — «Сохранить как шаблон»
|
|
648
|
+
// переехал на welcome-screen в ПКМ карточки проекта.)
|
|
677
649
|
setCloudButtonsVisibility();
|
|
678
650
|
// Переинициализируем видимость кнопок раз в 5 сек — для случая когда юзер
|
|
679
651
|
// login/logout в Chatium через настройки (нет других сигналов).
|
package/renderer/styles.css
CHANGED
|
@@ -269,19 +269,24 @@
|
|
|
269
269
|
}
|
|
270
270
|
.chat-header button:hover { background: #2a2a2a; color: #fff; }
|
|
271
271
|
.chat-list {
|
|
272
|
-
flex: 1; overflow-y: auto; padding:
|
|
273
|
-
display: flex; flex-direction: column; gap:
|
|
272
|
+
flex: 1; overflow-y: auto; padding: 10px;
|
|
273
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
274
274
|
}
|
|
275
275
|
.chat-msg {
|
|
276
|
-
display: flex; flex-direction: column; gap:
|
|
277
|
-
padding: 10px
|
|
278
|
-
font-size: 13px; line-height: 1.
|
|
276
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
277
|
+
padding: 6px 10px; border-radius: 6px;
|
|
278
|
+
font-size: 13px; line-height: 1.45;
|
|
279
279
|
}
|
|
280
280
|
.chat-msg-user { background: #1f2a3a; border: 1px solid #2a3a4a; }
|
|
281
281
|
.chat-msg-assistant { background: #1f1f1f; border: 1px solid #2a2a2a; }
|
|
282
282
|
.chat-msg-role {
|
|
283
283
|
font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
284
284
|
color: #888;
|
|
285
|
+
display: flex; align-items: center; gap: 6px;
|
|
286
|
+
}
|
|
287
|
+
.chat-msg-avatar {
|
|
288
|
+
width: 16px; height: 16px; border-radius: 4px;
|
|
289
|
+
object-fit: contain; background: #1f1f1f; padding: 1px;
|
|
285
290
|
}
|
|
286
291
|
.chat-msg-user .chat-msg-role { color: #6a9; }
|
|
287
292
|
.chat-msg-assistant .chat-msg-role { color: #c97; }
|
|
@@ -289,8 +294,8 @@
|
|
|
289
294
|
color: #e0e0e0; white-space: pre-wrap; word-break: break-word;
|
|
290
295
|
}
|
|
291
296
|
.chat-tool {
|
|
292
|
-
margin-top:
|
|
293
|
-
border-radius:
|
|
297
|
+
margin-top: 2px; background: #0e0e0e; border: 1px solid #2a2a2a;
|
|
298
|
+
border-radius: 3px; padding: 2px 6px; font-size: 11px;
|
|
294
299
|
}
|
|
295
300
|
.chat-tool summary {
|
|
296
301
|
cursor: pointer; color: #aac; outline: none;
|