kingkont 0.7.63 → 0.7.64
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/index.html +11 -27
- package/lib/cli.js +20 -9
- package/lib/projectFs.js +19 -1
- package/package.json +1 -1
- package/renderer/board.js +101 -35
- package/renderer/generate.js +52 -32
- package/renderer/state.js +41 -0
- package/renderer/styles.css +36 -0
- package/skill/SKILL.md +23 -10
package/index.html
CHANGED
|
@@ -440,16 +440,8 @@
|
|
|
440
440
|
<label id="locPickRow" style="display:none;">Локация
|
|
441
441
|
<select id="locPickSelect"><option value="">— не выбрана —</option></select>
|
|
442
442
|
</label>
|
|
443
|
-
<!--
|
|
444
|
-
<div id="
|
|
445
|
-
<label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer; padding:8px 10px; background:#1a1a1a; border:1px solid #333; border-radius:6px;">
|
|
446
|
-
<input type="checkbox" id="genDefaultPromptToggle" checked style="margin-top:2px; flex-shrink:0; accent-color:#7c3aed;">
|
|
447
|
-
<div style="flex:1; min-width:0;">
|
|
448
|
-
<div style="font-size:11px; color:#888; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:3px;">Дефолт сцены — добавится к промпту при генерации</div>
|
|
449
|
-
<div id="genDefaultPromptText" style="font-size:12px; color:#aaccdd; font-family:ui-monospace,monospace; white-space:pre-wrap; word-break:break-word;"></div>
|
|
450
|
-
</div>
|
|
451
|
-
</label>
|
|
452
|
-
</div>
|
|
443
|
+
<!-- Дефолтные промпты сцены (стек чекбоксов; рендерится динамически) -->
|
|
444
|
+
<div id="genDefaultPromptsRow" style="display:none; margin-bottom:8px; flex-direction:column; gap:6px;"></div>
|
|
453
445
|
<label><span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block;">Описание · <span style="color:#aae06a; font-family:ui-monospace,monospace; text-transform:none;">@имя</span> для ссылки на ноду</span>
|
|
454
446
|
<textarea id="genPrompt" placeholder="Опиши, что должно быть. Печатай @ чтобы вставить ссылку на ноду..."></textarea>
|
|
455
447
|
<div id="mentionPopup" class="mention-popup hidden"></div>
|
|
@@ -465,27 +457,19 @@
|
|
|
465
457
|
</div>
|
|
466
458
|
|
|
467
459
|
<!-- ===== Полноэкранный просмотр (image/video) ===== -->
|
|
468
|
-
<!-- ===== Дефолтные промпты сцены (
|
|
460
|
+
<!-- ===== Дефолтные промпты сцены (multi, с тегами kind) ===== -->
|
|
469
461
|
<div class="modal hidden" id="defaultPromptsModal">
|
|
470
|
-
<div class="modal-card" style="min-width:
|
|
462
|
+
<div class="modal-card" style="min-width:600px; max-width:760px;">
|
|
471
463
|
<h3 id="defaultPromptsTitle">Дефолтные промпты сцены</h3>
|
|
472
464
|
<div class="hint" style="margin-bottom:14px;">
|
|
473
|
-
|
|
474
|
-
в поле промпта —
|
|
475
|
-
|
|
465
|
+
Промпты добавляются к каждой генерации соответствующего типа в этой
|
|
466
|
+
сцене (НЕ редактируются в поле промпта — применяются в момент генерации).
|
|
467
|
+
В gen-модалке каждый можно отдельно включать/отключать.
|
|
468
|
+
</div>
|
|
469
|
+
<div id="dpList" style="display:flex; flex-direction:column; gap:12px;"></div>
|
|
470
|
+
<div style="margin-top:10px;">
|
|
471
|
+
<button id="dpAdd" type="button">+ Добавить промпт</button>
|
|
476
472
|
</div>
|
|
477
|
-
<label>Картинка
|
|
478
|
-
<textarea id="dpImage" rows="2" placeholder="например: cinematic lighting, soft shadows" style="width:100%;font-family:inherit;"></textarea>
|
|
479
|
-
</label>
|
|
480
|
-
<label>Видео
|
|
481
|
-
<textarea id="dpVideo" rows="2" placeholder="например: smooth camera movement, 24fps cinematic" style="width:100%;font-family:inherit;"></textarea>
|
|
482
|
-
</label>
|
|
483
|
-
<label>Аудио (TTS / SFX / музыка)
|
|
484
|
-
<textarea id="dpAudio" rows="2" placeholder="например: тёплый, мягкий тон" style="width:100%;font-family:inherit;"></textarea>
|
|
485
|
-
</label>
|
|
486
|
-
<label>Текст
|
|
487
|
-
<textarea id="dpText" rows="2" placeholder="например: пиши кратко, в стиле сценария" style="width:100%;font-family:inherit;"></textarea>
|
|
488
|
-
</label>
|
|
489
473
|
<div class="modal-actions">
|
|
490
474
|
<button id="dpCancel">Отмена</button>
|
|
491
475
|
<button id="dpSave" class="primary">Сохранить</button>
|
package/lib/cli.js
CHANGED
|
@@ -202,17 +202,28 @@ async function cmdGen({ positional, flags }) {
|
|
|
202
202
|
await fsLib.saveScene(root, ref, scene);
|
|
203
203
|
console.error(`[node] создана ${node.id} (status=generating)`);
|
|
204
204
|
|
|
205
|
-
// Применяем scene.settings.defaultPrompts
|
|
206
|
-
//
|
|
207
|
-
//
|
|
205
|
+
// Применяем дефолтные промпты сцены — массив scene.settings.defaultPrompts
|
|
206
|
+
// [{id, text, kinds, enabled}]. Берём только те, у которых enabled!==false
|
|
207
|
+
// и в kinds есть текущий kind. Backward-compat: если settings.defaultPrompts
|
|
208
|
+
// — старый object-формат {image: "..."}, конвертируем на лету.
|
|
208
209
|
let effectivePrompt = prompt;
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
const rawDp = scene.settings?.defaultPrompts;
|
|
211
|
+
const dpArr = Array.isArray(rawDp)
|
|
212
|
+
? rawDp
|
|
213
|
+
: (rawDp && typeof rawDp === 'object'
|
|
214
|
+
? Object.entries(rawDp).filter(([_, v]) => v && String(v).trim()).map(([k, v]) => ({
|
|
215
|
+
text: String(v).trim(), kinds: [k], enabled: true,
|
|
216
|
+
}))
|
|
217
|
+
: []);
|
|
218
|
+
const noDefault = flags['no-default-prompt'] || flags.noDefaultPrompt;
|
|
219
|
+
const applicable = dpArr.filter(p =>
|
|
220
|
+
p && p.enabled !== false && (p.kinds || []).includes(kind) && p.text
|
|
221
|
+
);
|
|
222
|
+
if (applicable.length && !noDefault) {
|
|
223
|
+
const prefix = applicable.map(p => String(p.text).trim()).join(', ');
|
|
213
224
|
const u = (prompt || '').trim();
|
|
214
|
-
effectivePrompt = u ? (
|
|
215
|
-
console.error(`[gen] applied
|
|
225
|
+
effectivePrompt = u ? (prefix + ', ' + u) : prefix;
|
|
226
|
+
console.error(`[gen] applied ${applicable.length} default prompt(s) for ${kind}: "${prefix.slice(0, 100)}${prefix.length > 100 ? '…' : ''}"`);
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
// 4) Запускаем генерацию через провайдер.
|
package/lib/projectFs.js
CHANGED
|
@@ -111,6 +111,24 @@ async function loadScene(root, ref) {
|
|
|
111
111
|
throw e;
|
|
112
112
|
}
|
|
113
113
|
const data = JSON.parse(raw);
|
|
114
|
+
// Backward-compat: settings.defaultPrompts может быть старым object-форматом
|
|
115
|
+
// ({image: "...", video: "..."}) или новым array ([{id, text, kinds, enabled}]).
|
|
116
|
+
// Нормализуем к array — CLI работает только с array.
|
|
117
|
+
let settings = data.settings || null;
|
|
118
|
+
if (settings && settings.defaultPrompts && !Array.isArray(settings.defaultPrompts)) {
|
|
119
|
+
const dp = settings.defaultPrompts;
|
|
120
|
+
const arr = [];
|
|
121
|
+
for (const [kindKey, text] of Object.entries(dp || {})) {
|
|
122
|
+
if (typeof text !== 'string' || !text.trim()) continue;
|
|
123
|
+
arr.push({
|
|
124
|
+
id: crypto.randomUUID(),
|
|
125
|
+
text: text.trim(),
|
|
126
|
+
kinds: [kindKey],
|
|
127
|
+
enabled: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
settings = { ...settings, defaultPrompts: arr };
|
|
131
|
+
}
|
|
114
132
|
const scene = {
|
|
115
133
|
nodes: Array.isArray(data.nodes) ? data.nodes : [],
|
|
116
134
|
connections: Array.isArray(data.connections) ? data.connections : [],
|
|
@@ -119,7 +137,7 @@ async function loadScene(root, ref) {
|
|
|
119
137
|
location: data.location || null,
|
|
120
138
|
timeline: data.timeline || null,
|
|
121
139
|
history: data.history || null,
|
|
122
|
-
settings
|
|
140
|
+
settings,
|
|
123
141
|
};
|
|
124
142
|
// Подгрузить .md для text-нод (как делает renderer в loadBoardMetadata).
|
|
125
143
|
for (const n of scene.nodes) {
|
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -724,11 +724,77 @@ async function promptBoardAspectRatio(kind, item) {
|
|
|
724
724
|
console.log(`[board] ${kind}/${item.name} aspectRatio → ${chosen}`);
|
|
725
725
|
}
|
|
726
726
|
|
|
727
|
-
//
|
|
728
|
-
// text)
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
727
|
+
// Open «Дефолтные промпты сцены» modal — multi-prompt list, each with
|
|
728
|
+
// kind-tags (image/video/audio/text) и default-enabled-чекбоксом.
|
|
729
|
+
// Сохраняется в scene.json → settings.defaultPrompts как массив.
|
|
730
|
+
const DP_KINDS = [
|
|
731
|
+
{ id: 'image', label: 'Картинка' },
|
|
732
|
+
{ id: 'video', label: 'Видео' },
|
|
733
|
+
{ id: 'audio', label: 'Аудио' },
|
|
734
|
+
{ id: 'text', label: 'Текст' },
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
function _dpRenderItem(prompt) {
|
|
738
|
+
const row = document.createElement('div');
|
|
739
|
+
row.className = 'dp-item';
|
|
740
|
+
row.dataset.id = prompt.id;
|
|
741
|
+
// Textarea для самого текста промпта.
|
|
742
|
+
const ta = document.createElement('textarea');
|
|
743
|
+
ta.value = prompt.text || '';
|
|
744
|
+
ta.placeholder = 'например: cinematic lighting, soft shadows';
|
|
745
|
+
ta.rows = 2;
|
|
746
|
+
row.appendChild(ta);
|
|
747
|
+
// Контролы: kind-чипы + дефолт-чекбокс + удалить.
|
|
748
|
+
const controls = document.createElement('div');
|
|
749
|
+
controls.className = 'dp-item-controls';
|
|
750
|
+
const kindsLabel = document.createElement('span');
|
|
751
|
+
kindsLabel.textContent = 'для:';
|
|
752
|
+
controls.appendChild(kindsLabel);
|
|
753
|
+
const kindsBox = document.createElement('div');
|
|
754
|
+
kindsBox.className = 'dp-item-kinds';
|
|
755
|
+
for (const k of DP_KINDS) {
|
|
756
|
+
const chip = document.createElement('span');
|
|
757
|
+
chip.className = 'dp-kind-chip';
|
|
758
|
+
chip.dataset.kind = k.id;
|
|
759
|
+
chip.textContent = k.label;
|
|
760
|
+
if ((prompt.kinds || []).includes(k.id)) chip.classList.add('active');
|
|
761
|
+
chip.addEventListener('click', () => chip.classList.toggle('active'));
|
|
762
|
+
kindsBox.appendChild(chip);
|
|
763
|
+
}
|
|
764
|
+
controls.appendChild(kindsBox);
|
|
765
|
+
const defLabel = document.createElement('label');
|
|
766
|
+
defLabel.className = 'dp-item-default';
|
|
767
|
+
const defCb = document.createElement('input');
|
|
768
|
+
defCb.type = 'checkbox';
|
|
769
|
+
defCb.checked = prompt.enabled !== false;
|
|
770
|
+
defLabel.appendChild(defCb);
|
|
771
|
+
defLabel.append(document.createTextNode('включён по умолчанию'));
|
|
772
|
+
controls.appendChild(defLabel);
|
|
773
|
+
const delBtn = document.createElement('button');
|
|
774
|
+
delBtn.type = 'button';
|
|
775
|
+
delBtn.className = 'dp-item-delete';
|
|
776
|
+
delBtn.title = 'Удалить';
|
|
777
|
+
delBtn.textContent = '×';
|
|
778
|
+
delBtn.addEventListener('click', () => row.remove());
|
|
779
|
+
controls.appendChild(delBtn);
|
|
780
|
+
row.appendChild(controls);
|
|
781
|
+
return row;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function _dpReadList() {
|
|
785
|
+
const list = document.getElementById('dpList');
|
|
786
|
+
const out = [];
|
|
787
|
+
for (const row of list.querySelectorAll('.dp-item')) {
|
|
788
|
+
const text = row.querySelector('textarea').value.trim();
|
|
789
|
+
if (!text) continue; // пустые отбрасываем
|
|
790
|
+
const kinds = [...row.querySelectorAll('.dp-kind-chip.active')].map(c => c.dataset.kind);
|
|
791
|
+
if (!kinds.length) continue; // без kind тоже не пишем — не виден нигде
|
|
792
|
+
const enabled = row.querySelector('.dp-item-default input').checked;
|
|
793
|
+
out.push({ id: row.dataset.id, text, kinds, enabled });
|
|
794
|
+
}
|
|
795
|
+
return out;
|
|
796
|
+
}
|
|
797
|
+
|
|
732
798
|
async function openDefaultPromptsDialog(kind, item) {
|
|
733
799
|
const isActive = state.currentBoard?.kind === kind && state.currentBoard.name === item.name;
|
|
734
800
|
let metaSettings;
|
|
@@ -738,56 +804,56 @@ async function openDefaultPromptsDialog(kind, item) {
|
|
|
738
804
|
const meta = await loadBoardMetadata(item.handle);
|
|
739
805
|
metaSettings = meta.settings || {};
|
|
740
806
|
}
|
|
741
|
-
const
|
|
742
|
-
// Заголовок и заполнение полей.
|
|
807
|
+
const prompts = (metaSettings.defaultPrompts || []);
|
|
743
808
|
const titleEl = document.getElementById('defaultPromptsTitle');
|
|
744
809
|
if (titleEl) titleEl.textContent = `Дефолтные промпты для «${item.name}»`;
|
|
745
|
-
|
|
746
|
-
document.getElementById('
|
|
747
|
-
|
|
748
|
-
|
|
810
|
+
// Рендерим существующие.
|
|
811
|
+
const list = document.getElementById('dpList');
|
|
812
|
+
list.innerHTML = '';
|
|
813
|
+
for (const p of prompts) list.appendChild(_dpRenderItem(p));
|
|
814
|
+
// Если ничего нет — сразу добавляем пустую заготовку, чтобы юзер не
|
|
815
|
+
// ломал голову куда печатать.
|
|
816
|
+
if (!prompts.length) {
|
|
817
|
+
list.appendChild(_dpRenderItem({ id: crypto.randomUUID(), text: '', kinds: ['image'], enabled: true }));
|
|
818
|
+
}
|
|
749
819
|
document.getElementById('defaultPromptsModal').classList.remove('hidden');
|
|
750
|
-
setTimeout(() =>
|
|
820
|
+
setTimeout(() => list.querySelector('textarea')?.focus(), 30);
|
|
751
821
|
|
|
752
|
-
// Save
|
|
822
|
+
// Save / Cancel / Add — навешиваем фрешевые handler'ы (с removeEventListener
|
|
823
|
+
// на close, чтобы при следующем openDefaultPromptsDialog не накапливались).
|
|
753
824
|
const saveBtn = document.getElementById('dpSave');
|
|
754
825
|
const cancelBtn = document.getElementById('dpCancel');
|
|
826
|
+
const addBtn = document.getElementById('dpAdd');
|
|
755
827
|
const close = () => document.getElementById('defaultPromptsModal').classList.add('hidden');
|
|
828
|
+
const cleanup = () => {
|
|
829
|
+
saveBtn.removeEventListener('click', onSave);
|
|
830
|
+
cancelBtn.removeEventListener('click', onCancel);
|
|
831
|
+
addBtn.removeEventListener('click', onAdd);
|
|
832
|
+
};
|
|
833
|
+
const onAdd = () => {
|
|
834
|
+
list.appendChild(_dpRenderItem({ id: crypto.randomUUID(), text: '', kinds: ['image'], enabled: true }));
|
|
835
|
+
list.lastElementChild.querySelector('textarea').focus();
|
|
836
|
+
};
|
|
756
837
|
const onSave = async () => {
|
|
757
|
-
const next =
|
|
758
|
-
image: document.getElementById('dpImage').value.trim(),
|
|
759
|
-
video: document.getElementById('dpVideo').value.trim(),
|
|
760
|
-
audio: document.getElementById('dpAudio').value.trim(),
|
|
761
|
-
text: document.getElementById('dpText').value.trim(),
|
|
762
|
-
};
|
|
763
|
-
// Удаляем пустые ключи чтобы не плодить мусор в JSON.
|
|
764
|
-
for (const k of Object.keys(next)) if (!next[k]) delete next[k];
|
|
838
|
+
const next = _dpReadList();
|
|
765
839
|
if (isActive) {
|
|
766
840
|
if (!state.currentBoard.metadata.settings) state.currentBoard.metadata.settings = {};
|
|
767
|
-
if (
|
|
768
|
-
|
|
769
|
-
} else {
|
|
770
|
-
delete state.currentBoard.metadata.settings.defaultPrompts;
|
|
771
|
-
}
|
|
841
|
+
if (next.length) state.currentBoard.metadata.settings.defaultPrompts = next;
|
|
842
|
+
else delete state.currentBoard.metadata.settings.defaultPrompts;
|
|
772
843
|
scheduleSave();
|
|
773
844
|
} else {
|
|
774
845
|
const meta = await loadBoardMetadata(item.handle);
|
|
775
846
|
meta.settings = meta.settings || {};
|
|
776
|
-
if (
|
|
847
|
+
if (next.length) meta.settings.defaultPrompts = next;
|
|
777
848
|
else delete meta.settings.defaultPrompts;
|
|
778
849
|
await saveBoardMetadata(item.handle, meta);
|
|
779
850
|
}
|
|
780
|
-
close();
|
|
781
|
-
saveBtn.removeEventListener('click', onSave);
|
|
782
|
-
cancelBtn.removeEventListener('click', onCancel);
|
|
783
|
-
};
|
|
784
|
-
const onCancel = () => {
|
|
785
|
-
close();
|
|
786
|
-
saveBtn.removeEventListener('click', onSave);
|
|
787
|
-
cancelBtn.removeEventListener('click', onCancel);
|
|
851
|
+
close(); cleanup();
|
|
788
852
|
};
|
|
853
|
+
const onCancel = () => { close(); cleanup(); };
|
|
789
854
|
saveBtn.addEventListener('click', onSave);
|
|
790
855
|
cancelBtn.addEventListener('click', onCancel);
|
|
856
|
+
addBtn.addEventListener('click', onAdd);
|
|
791
857
|
}
|
|
792
858
|
|
|
793
859
|
// Переименовать board (папку): создать новую папку с тем же именем, перенести
|
package/renderer/generate.js
CHANGED
|
@@ -809,45 +809,65 @@ function nearestSupportedAspect(ratio) {
|
|
|
809
809
|
return best.name;
|
|
810
810
|
}
|
|
811
811
|
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
812
|
+
// Рендерим стек дефолтных промптов в gen-modal'е. Каждый промпт,
|
|
813
|
+
// у которого в `kinds` есть текущий kind, показывается отдельной строкой
|
|
814
|
+
// с чекбоксом. Initial state — prompt.enabled (заданный в scene-settings).
|
|
815
|
+
// Состояние «включён сейчас» (per-open, не sticky) хранится в Set
|
|
816
|
+
// state.activeDefaultPrompts (id'ы включённых).
|
|
816
817
|
function syncDefaultPromptRow() {
|
|
817
|
-
const row = document.getElementById('
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
if (
|
|
824
|
-
text.textContent = value;
|
|
825
|
-
toggle.checked = true;
|
|
826
|
-
state.useDefaultPrompt = true;
|
|
827
|
-
row.style.display = '';
|
|
828
|
-
} else {
|
|
818
|
+
const row = document.getElementById('genDefaultPromptsRow');
|
|
819
|
+
if (!row) return;
|
|
820
|
+
row.innerHTML = '';
|
|
821
|
+
state.activeDefaultPrompts = new Set();
|
|
822
|
+
const all = state.currentBoard?.metadata?.settings?.defaultPrompts || [];
|
|
823
|
+
const prompts = all.filter(p => (p.kinds || []).includes(state.genKind) && p.text);
|
|
824
|
+
if (!prompts.length) {
|
|
829
825
|
row.style.display = 'none';
|
|
830
|
-
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
row.style.display = 'flex';
|
|
829
|
+
for (const p of prompts) {
|
|
830
|
+
if (p.enabled !== false) state.activeDefaultPrompts.add(p.id);
|
|
831
|
+
const lab = document.createElement('label');
|
|
832
|
+
lab.style.cssText = 'display:flex; align-items:flex-start; gap:8px; cursor:pointer; padding:8px 10px; background:#1a1a1a; border:1px solid #333; border-radius:6px;';
|
|
833
|
+
const cb = document.createElement('input');
|
|
834
|
+
cb.type = 'checkbox';
|
|
835
|
+
cb.checked = p.enabled !== false;
|
|
836
|
+
cb.style.cssText = 'margin-top:2px; flex-shrink:0; accent-color:#7c3aed;';
|
|
837
|
+
cb.addEventListener('change', () => {
|
|
838
|
+
if (cb.checked) state.activeDefaultPrompts.add(p.id);
|
|
839
|
+
else state.activeDefaultPrompts.delete(p.id);
|
|
840
|
+
});
|
|
841
|
+
lab.appendChild(cb);
|
|
842
|
+
const wrap = document.createElement('div');
|
|
843
|
+
wrap.style.cssText = 'flex:1; min-width:0;';
|
|
844
|
+
const cap = document.createElement('div');
|
|
845
|
+
cap.style.cssText = 'font-size:11px; color:#888; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:3px;';
|
|
846
|
+
cap.textContent = 'дефолт сцены — добавится к промпту';
|
|
847
|
+
wrap.appendChild(cap);
|
|
848
|
+
const txt = document.createElement('div');
|
|
849
|
+
txt.style.cssText = 'font-size:12px; color:#aaccdd; font-family:ui-monospace,monospace; white-space:pre-wrap; word-break:break-word;';
|
|
850
|
+
txt.textContent = p.text;
|
|
851
|
+
wrap.appendChild(txt);
|
|
852
|
+
lab.appendChild(wrap);
|
|
853
|
+
row.appendChild(lab);
|
|
831
854
|
}
|
|
832
855
|
}
|
|
833
856
|
|
|
834
|
-
//
|
|
835
|
-
|
|
836
|
-
if (_dpToggleEl) _dpToggleEl.addEventListener('change', () => {
|
|
837
|
-
state.useDefaultPrompt = _dpToggleEl.checked;
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
// Применяет дефолтный промпт сцены к user-prompt'у. Префиксует с разделителем
|
|
841
|
-
// ", " (типичная конвенция для image/video моделей). Если default отключён
|
|
842
|
-
// или пуст — возвращает prompt как есть.
|
|
857
|
+
// Применяет включённые дефолтные промпты сцены к user-prompt'у.
|
|
858
|
+
// Префиксует с разделителем ", ". Промпты идут в порядке как в settings.
|
|
843
859
|
function applyDefaultPrompt(userPrompt, kind) {
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
860
|
+
const all = state.currentBoard?.metadata?.settings?.defaultPrompts || [];
|
|
861
|
+
const active = state.activeDefaultPrompts || new Set();
|
|
862
|
+
const apply = all
|
|
863
|
+
.filter(p => active.has(p.id) && (p.kinds || []).includes(kind) && p.text)
|
|
864
|
+
.map(p => p.text.trim())
|
|
865
|
+
.filter(Boolean);
|
|
866
|
+
if (!apply.length) return userPrompt;
|
|
867
|
+
const prefix = apply.join(', ');
|
|
848
868
|
const u = (userPrompt || '').trim();
|
|
849
|
-
if (!u) return
|
|
850
|
-
return
|
|
869
|
+
if (!u) return prefix;
|
|
870
|
+
return prefix + ', ' + u;
|
|
851
871
|
}
|
|
852
872
|
|
|
853
873
|
// === Source frame controls ===
|
package/renderer/state.js
CHANGED
|
@@ -301,9 +301,50 @@ async function loadBoardMetadata(boardHandle) {
|
|
|
301
301
|
try { await boardHandle.removeEntry('.editor.json'); } catch {}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// Нормализуем settings.defaultPrompts к новому array-формату
|
|
305
|
+
// (старый object-формат миграется на лету; saveBoardMetadata всегда
|
|
306
|
+
// пишет в новом).
|
|
307
|
+
if (settings && settings.defaultPrompts !== undefined) {
|
|
308
|
+
const normalized = normalizeDefaultPrompts(settings.defaultPrompts);
|
|
309
|
+
if (JSON.stringify(normalized) !== JSON.stringify(settings.defaultPrompts)) {
|
|
310
|
+
migrated = true; // запишется на следующем save в новом формате
|
|
311
|
+
}
|
|
312
|
+
settings = { ...settings, defaultPrompts: normalized };
|
|
313
|
+
}
|
|
304
314
|
return { nodes, connections, view, character, location, timeline, history, settings, _migrated: migrated };
|
|
305
315
|
}
|
|
306
316
|
|
|
317
|
+
// Backward-compat: старый формат settings.defaultPrompts был
|
|
318
|
+
// `{ image: "...", video: "...", ... }` — по одному промпту на kind.
|
|
319
|
+
// Новый формат — массив `[{ id, text, kinds: [...], enabled: true }, ...]`.
|
|
320
|
+
// Нормализуем при чтении: если object — конвертируем в array, каждый
|
|
321
|
+
// non-empty key становится одним prompt'ом со своим единственным kind.
|
|
322
|
+
function normalizeDefaultPrompts(dp) {
|
|
323
|
+
if (Array.isArray(dp)) {
|
|
324
|
+
// Гарантируем что у каждого есть id и enabled.
|
|
325
|
+
return dp.map(p => ({
|
|
326
|
+
id: p.id || crypto.randomUUID(),
|
|
327
|
+
text: p.text || '',
|
|
328
|
+
kinds: Array.isArray(p.kinds) && p.kinds.length ? p.kinds : ['image'],
|
|
329
|
+
enabled: p.enabled !== false, // default true
|
|
330
|
+
...(p.name ? { name: p.name } : {}),
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
if (!dp || typeof dp !== 'object') return [];
|
|
334
|
+
// Старый object-формат → array.
|
|
335
|
+
const out = [];
|
|
336
|
+
for (const [kind, text] of Object.entries(dp)) {
|
|
337
|
+
if (typeof text !== 'string' || !text.trim()) continue;
|
|
338
|
+
out.push({
|
|
339
|
+
id: crypto.randomUUID(),
|
|
340
|
+
text: text.trim(),
|
|
341
|
+
kinds: [kind],
|
|
342
|
+
enabled: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
307
348
|
async function saveBoardMetadata(boardHandle, meta) {
|
|
308
349
|
for (const n of meta.nodes) {
|
|
309
350
|
if (n.type === 'text' && n.file) {
|
package/renderer/styles.css
CHANGED
|
@@ -463,6 +463,42 @@
|
|
|
463
463
|
}
|
|
464
464
|
.fs-modal #fsClose:hover { background: rgba(255,68,68,0.7); }
|
|
465
465
|
|
|
466
|
+
/* Default-prompts editor: каждый промпт — карточка с textarea, kind-чипами,
|
|
467
|
+
дефолт-чекбоксом и кнопкой удаления. */
|
|
468
|
+
.dp-item {
|
|
469
|
+
border: 1px solid #333; border-radius: 8px; background: #1a1a1a;
|
|
470
|
+
padding: 10px 12px; display: flex; flex-direction: column; gap: 8px;
|
|
471
|
+
}
|
|
472
|
+
.dp-item textarea {
|
|
473
|
+
width: 100%; font-family: ui-monospace, monospace; font-size: 12px;
|
|
474
|
+
background: #0e0e0e; color: #e0e0e0; border: 1px solid #333; border-radius: 4px;
|
|
475
|
+
padding: 6px 8px; resize: vertical; min-height: 40px;
|
|
476
|
+
}
|
|
477
|
+
.dp-item-controls {
|
|
478
|
+
display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
|
|
479
|
+
font-size: 12px; color: #aaa;
|
|
480
|
+
}
|
|
481
|
+
.dp-item-kinds {
|
|
482
|
+
display: flex; gap: 4px;
|
|
483
|
+
}
|
|
484
|
+
.dp-kind-chip {
|
|
485
|
+
padding: 3px 9px; border: 1px solid #444; border-radius: 12px;
|
|
486
|
+
background: #1a1a1a; color: #888; font-size: 11px; cursor: pointer;
|
|
487
|
+
user-select: none;
|
|
488
|
+
}
|
|
489
|
+
.dp-kind-chip.active {
|
|
490
|
+
background: rgba(124,58,237,0.18); border-color: #7c3aed; color: #c8a8ff;
|
|
491
|
+
}
|
|
492
|
+
.dp-item-default {
|
|
493
|
+
display: flex; align-items: center; gap: 6px; cursor: pointer;
|
|
494
|
+
}
|
|
495
|
+
.dp-item-default input { accent-color: #7c3aed; }
|
|
496
|
+
.dp-item-delete {
|
|
497
|
+
margin-left: auto; background: transparent; border: 0; color: #888;
|
|
498
|
+
font-size: 18px; line-height: 1; cursor: pointer; padding: 2px 6px;
|
|
499
|
+
}
|
|
500
|
+
.dp-item-delete:hover { color: #f88; }
|
|
501
|
+
|
|
466
502
|
/* Кнопки навигации prev/next в полноэкранном просмотре */
|
|
467
503
|
.fs-modal .fs-nav {
|
|
468
504
|
position: absolute; top: 50%; transform: translateY(-50%);
|
package/skill/SKILL.md
CHANGED
|
@@ -105,19 +105,32 @@ kingkont gen <project> 'Серия 1' --kind=image --prompt="..." --aspect-ratio
|
|
|
105
105
|
|
|
106
106
|
### Дефолтные промпты сцены
|
|
107
107
|
|
|
108
|
-
`scene.json → settings.defaultPrompts` —
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
`scene.json → settings.defaultPrompts` — массив:
|
|
109
|
+
```json
|
|
110
|
+
[
|
|
111
|
+
{ "id": "uuid", "text": "cinematic lighting", "kinds": ["image","video"], "enabled": true },
|
|
112
|
+
{ "id": "uuid", "text": "тёплый тон", "kinds": ["audio"], "enabled": false }
|
|
113
|
+
]
|
|
114
|
+
```
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
116
|
+
Каждый prompt:
|
|
117
|
+
- `text` — собственно строка
|
|
118
|
+
- `kinds` — список kind'ов где он применим (`image / video / audio / text`)
|
|
119
|
+
- `enabled` — true/false: применяется ли по умолчанию (юзер может в gen-modal'е переключить для конкретной генерации)
|
|
120
|
+
|
|
121
|
+
CLI `kingkont gen <kind>` берёт ВСЕ промпты с `enabled !== false` и
|
|
122
|
+
текущим kind в их `kinds`, конкатенирует через `, ` и префиксует к
|
|
123
|
+
user-prompt'у. Чтобы отключить целиком для одной генерации — флаг
|
|
124
|
+
`--no-default-prompt`. В stderr пишется `[gen] applied N default
|
|
125
|
+
prompt(s) for <kind>: "..."`.
|
|
117
126
|
|
|
118
127
|
Юзер обычно настраивает defaults через UI (правый клик на сцену →
|
|
119
|
-
«📝 Дефолтные промпты…»)
|
|
120
|
-
|
|
128
|
+
«📝 Дефолтные промпты…»). Через CLI — правь `scene.json` напрямую,
|
|
129
|
+
добавляя/удаляя элементы массива.
|
|
130
|
+
|
|
131
|
+
Backward-compat: если в scene.json остался старый формат
|
|
132
|
+
`settings.defaultPrompts: { image: "..." }` — CLI и UI конвертируют
|
|
133
|
+
его в массив на лету; следующее сохранение перепишет в новый формат.
|
|
121
134
|
|
|
122
135
|
## ⚠️ Text-ноды — генерируй САМ, не через `kingkont gen --kind=text`
|
|
123
136
|
|