kingkont 0.7.66 → 0.7.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/renderer/media.js +85 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.66",
3
+ "version": "0.7.67",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/media.js CHANGED
@@ -1119,40 +1119,81 @@ function updateMentionPopup() {
1119
1119
  let items = [];
1120
1120
 
1121
1121
  if (dotIdx >= 0) {
1122
- // Уже выбран character ИЛИ location показываем его картинки.
1122
+ // Уже выбран character / location / __scene (текущая доска) — drilldown.
1123
1123
  const ownerName = query.slice(0, dotIdx);
1124
1124
  const imgQuery = query.slice(dotIdx + 1);
1125
- const c = state.charactersInfo.find(x => x.name.toLowerCase() === ownerName);
1126
- if (c) {
1127
- for (const img of (c.imageNodes || [])) {
1128
- if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
1129
- items.push({
1130
- key: `${c.name}.${img.name}`,
1131
- label: img.name,
1132
- type: 'image',
1133
- scope: 'char-image',
1134
- charName: c.name,
1135
- file: img.file,
1136
- boardHandle: c.handle,
1137
- });
1125
+ if (ownerName === '__scene') {
1126
+ // Drilldown в ноды ТЕКУЩЕЙ доски (тип фильтруем по genKind).
1127
+ const allowed = state.genKind === 'image' ? ['image', 'text']
1128
+ : state.genKind === 'video' ? ['image', 'video', 'audio', 'text']
1129
+ : ['text', 'image', 'video', 'audio'];
1130
+ // Дедуп: если у двух нод одинаковый label (basename), второй
1131
+ // получает суффикс -<id4>. Это match'ит логику _scanBoardForRefs
1132
+ // для chars/locs.
1133
+ const seen = new Set();
1134
+ for (const n of state.currentBoard.metadata.nodes) {
1135
+ if (!n.id) continue;
1136
+ if (!allowed.includes(n.type)) continue;
1137
+ let label = (n.name || '').trim();
1138
+ if (!label) {
1139
+ label = n.file
1140
+ ? (n.file.split('/').pop() || '').replace(/\.[^.]+$/, '')
1141
+ : n.id.slice(0, 8);
1138
1142
  }
1143
+ if (!label) label = n.id.slice(0, 8);
1144
+ if (seen.has(label)) label = `${label}-${n.id.slice(0, 4)}`;
1145
+ seen.add(label);
1146
+ if (imgQuery && !label.toLowerCase().includes(imgQuery)) continue;
1147
+ items.push({
1148
+ // key = label: красивая ссылка типа [@kitchen] для безымянной
1149
+ // картинки kitchen.jpg. resolveMentions для board-нод сначала
1150
+ // ищет по name; если не нашёл — gatherMediaRefs упустит,
1151
+ // но мы переименуем после: для board-drill final key = label.
1152
+ // Чтобы он попал в gatherMediaRefs — добавим временный n.name
1153
+ // ниже. На самом деле проще: всегда insert = [@<label>], и в
1154
+ // gatherMediaRefs сравнение идёт по nodeRefKey = name||id.
1155
+ // Если label != name && != id — НЕ попадёт. Поэтому используем
1156
+ // n.name || n.id (id как fallback) — это работает в
1157
+ // resolveMentions.
1158
+ key: n.name || n.id,
1159
+ label, type: n.type, scope: 'board',
1160
+ file: n.file || null,
1161
+ boardHandle: state.currentBoard.handle,
1162
+ });
1139
1163
  }
1140
1164
  } else {
1141
- const l = state.locationsInfo.find(x => x.name.toLowerCase() === ownerName);
1142
- if (l) {
1143
- for (const img of (l.imageNodes || [])) {
1165
+ const c = state.charactersInfo.find(x => x.name.toLowerCase() === ownerName);
1166
+ if (c) {
1167
+ for (const img of (c.imageNodes || [])) {
1144
1168
  if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
1145
1169
  items.push({
1146
- key: `${l.name}.${img.name}`,
1170
+ key: `${c.name}.${img.name}`,
1147
1171
  label: img.name,
1148
1172
  type: 'image',
1149
- scope: 'loc-image',
1150
- locName: l.name,
1173
+ scope: 'char-image',
1174
+ charName: c.name,
1151
1175
  file: img.file,
1152
- boardHandle: l.handle,
1176
+ boardHandle: c.handle,
1153
1177
  });
1154
1178
  }
1155
1179
  }
1180
+ } else {
1181
+ const l = state.locationsInfo.find(x => x.name.toLowerCase() === ownerName);
1182
+ if (l) {
1183
+ for (const img of (l.imageNodes || [])) {
1184
+ if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
1185
+ items.push({
1186
+ key: `${l.name}.${img.name}`,
1187
+ label: img.name,
1188
+ type: 'image',
1189
+ scope: 'loc-image',
1190
+ locName: l.name,
1191
+ file: img.file,
1192
+ boardHandle: l.handle,
1193
+ });
1194
+ }
1195
+ }
1196
+ }
1156
1197
  }
1157
1198
  }
1158
1199
  } else if (isCharOnly) {
@@ -1173,17 +1214,26 @@ function updateMentionPopup() {
1173
1214
  }
1174
1215
  items = items.filter(s => s.key.toLowerCase().includes(query)).slice(0, 16);
1175
1216
  } else {
1176
- // Верхний уровень: локальные именованные ноды + персонажи (одна запись на персонажа)
1177
- // Текстовые ноды разрешены везде resolveMentions инлайнит их .md в промпт.
1217
+ // Верхний уровень: вместо плоского списка нод текущей доски
1218
+ // одна запись «Эта сцена» (drilldown в её ноды). Это убирает
1219
+ // лишний шум когда в проекте много персонажей/локаций. Click →
1220
+ // selectMention вставит [@__scene. и реоткроет popup в drilldown.
1221
+ // Если у текущей доски НЕТ нод вообще — пункт не показываем.
1178
1222
  const allowed = state.genKind === 'image' ? ['image', 'text']
1179
1223
  : state.genKind === 'video' ? ['image', 'video', 'audio', 'text']
1180
1224
  : ['text', 'image', 'video', 'audio'];
1181
- for (const n of state.currentBoard.metadata.nodes) {
1182
- if (!n.name || !n.id) continue;
1183
- if (!allowed.includes(n.type)) continue;
1225
+ const boardHasNodes = (state.currentBoard?.metadata?.nodes || []).some(n =>
1226
+ n.id && allowed.includes(n.type)
1227
+ );
1228
+ if (boardHasNodes) {
1229
+ // Превью первой image-ноды для миниатюры (если есть).
1230
+ const firstImg = state.currentBoard.metadata.nodes.find(n => n.type === 'image' && n.file);
1184
1231
  items.push({
1185
- key: n.name, label: n.name, type: n.type, scope: 'board',
1186
- file: n.file || null,
1232
+ key: '__scene__top', // не для вставки drillFromSceneFolder заменит
1233
+ label: 'Эта сцена',
1234
+ type: 'image', // для type-чипа
1235
+ scope: 'scene-folder',
1236
+ file: firstImg?.file || null,
1187
1237
  boardHandle: state.currentBoard.handle,
1188
1238
  });
1189
1239
  }
@@ -1251,6 +1301,7 @@ function updateMentionPopup() {
1251
1301
  : s.scope === 'char-image' ? `персонаж·${s.charName}`
1252
1302
  : s.scope === 'loc' ? 'локация'
1253
1303
  : s.scope === 'loc-image' ? `локация·${s.locName}`
1304
+ : s.scope === 'scene-folder' ? 'сцена'
1254
1305
  : s.type;
1255
1306
  const nm = document.createElement('span');
1256
1307
  nm.textContent = s.label;
@@ -1301,11 +1352,17 @@ function selectMention(idx = mentionState.selected) {
1301
1352
  const after = ta.value.slice(cursor);
1302
1353
  // Если выбрали персонажа/локацию и у них есть картинки — авто-drill:
1303
1354
  // оставляем @name. для дальнейшего ввода имени картинки.
1355
+ // Если выбрали «Эта сцена» (scope='scene-folder') — то же самое со
1356
+ // спец-префиксом __scene. Финальный select board-ноды стирает префикс
1357
+ // и вставляет [@nodename] (см. ветку drillFromScene ниже).
1304
1358
  let insert;
1305
1359
  let reopen = false;
1306
1360
  if ((item.scope === 'char' || item.scope === 'loc') && item.hasImages) {
1307
1361
  insert = '[@' + item.key + '.';
1308
1362
  reopen = true;
1363
+ } else if (item.scope === 'scene-folder') {
1364
+ insert = '[@__scene.';
1365
+ reopen = true;
1309
1366
  } else {
1310
1367
  insert = '[@' + (item.key || item.name) + '] ';
1311
1368
  }