mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into parser-post-stuff
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,3 +47,4 @@ access.log
|
||||
public/css/user.css
|
||||
/plugins/
|
||||
/data
|
||||
/default/scaffold
|
||||
|
@ -231,6 +231,7 @@
|
||||
"api_url_scale": "",
|
||||
"show_external_models": false,
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.",
|
||||
"use_ai21_tokenizer": false,
|
||||
"use_google_tokenizer": false,
|
||||
|
@ -624,6 +624,7 @@
|
||||
"show_external_models": false,
|
||||
"proxy_password": "",
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"use_ai21_tokenizer": false
|
||||
}
|
||||
}
|
||||
|
26
default/scaffold/README.md
Normal file
26
default/scaffold/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Content Scaffolding
|
||||
|
||||
Content files in this folder will be copied for all users (old and new) on the server startup.
|
||||
|
||||
1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content.
|
||||
2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed.
|
||||
3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"filename": "themes/Midnight.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/city.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "characters/Charlie.png",
|
||||
"type": "character"
|
||||
}
|
||||
]
|
||||
```
|
@ -103,7 +103,8 @@
|
||||
}
|
||||
|
||||
#bulkTagsList,
|
||||
#tagList .tag {
|
||||
#tagList .tag,
|
||||
#groupTagList .tag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@
|
||||
</h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_novel" class="flex1 text_pole" data-preset-manager-for="novel">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
<option value="gui" data-i18n="Default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input type="file" hidden data-preset-manager-file="novel" accept=".json, .settings">
|
||||
@ -134,7 +134,7 @@
|
||||
<h4 class="margin0"><span data-i18n="openaipresets">Chat Completion Presets</span></h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_openai" class="flex1 text_pole" data-preset-manager-for="openai">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
<option value="gui" data-i18n="Default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input id="openai_preset_import_file" type="file" accept=".json,.settings" hidden />
|
||||
@ -246,7 +246,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title" data-i18n="temperature">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
@ -1268,13 +1268,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" name="dynaTempBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" data-i18n="DynaTemp">
|
||||
<div class="flex-container alignitemscenter" style="justify-content: center;">
|
||||
<h4 class="wide100p textAlignCenter">
|
||||
<div class="flex-container alignitemscenter justifyCenter">
|
||||
<div class="checkbox_label" for="dynatemp_textgenerationwebui">
|
||||
<input type="checkbox" id="dynatemp_textgenerationwebui" />
|
||||
<small data-i18n="dynatemp"></small>
|
||||
</div>
|
||||
<span style="text-align: center;" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
|
||||
<span class="textAlignCenter" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Scale Temperature dynamically per token, based on the variation of probabilities" title="Scale Temperature dynamically per token, based on the variation of probabilities."></div>
|
||||
</div>
|
||||
</h4>
|
||||
@ -1459,7 +1458,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby" class="wide100p">
|
||||
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby, llamacpp" class="wide100p">
|
||||
<hr class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter"><span data-i18n="JSON Schema">JSON Schema</span>
|
||||
<a href="https://json-schema.org/learn/getting-started-step-by-step" target="_blank">
|
||||
@ -1734,6 +1733,8 @@
|
||||
<div class="wide100p">
|
||||
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
|
||||
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
<span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
|
||||
<textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
</div>
|
||||
<label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
|
||||
<input id="claude_use_sysprompt" type="checkbox" />
|
||||
@ -3443,8 +3444,8 @@
|
||||
</label>
|
||||
<label title="If the entry key consists of only one word, it would not be matched as part of other words" data-i18n="[title]If the entry key consists of only one word, it would not be matched as part of other words" class="checkbox_label flex1">
|
||||
<input id="world_info_match_whole_words" type="checkbox" />
|
||||
<small data-i18n="Match whole words" class="whitespacenowrap flex1">
|
||||
Match whole words
|
||||
<small data-i18n="Match Whole Words" class="whitespacenowrap flex1">
|
||||
Match Whole Words
|
||||
</small>
|
||||
</label>
|
||||
<label title="Only the entries with the most number of key matches will be selected for Inclusion Group filtering" data-i18n="[title]Only the entries with the most number of key matches will be selected for Inclusion Group filtering" class="checkbox_label flex1">
|
||||
@ -3597,7 +3598,7 @@
|
||||
<div class="flex-container">
|
||||
<span data-i18n="Chat Style:">Chat Style:</span><br>
|
||||
<select id="chat_display" class="widthNatural flex1 margin0">
|
||||
<option value="0" data-i18n="Default">Flat</span>
|
||||
<option value="0" data-i18n="Flat">Flat</span>
|
||||
<option value="1" data-i18n="Bubbles">Bubbles</option>
|
||||
<option value="2" data-i18n="Document">Document</option>
|
||||
</select>
|
||||
@ -4082,7 +4083,7 @@
|
||||
<small class="fa-solid fa-circle-question note-link-small"></small>
|
||||
</a>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disalow embedded media from other domains in chat messages." data-i18n="[title]Disalow embedded media from other domains in chat messages">
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disallow embedded media from other domains in chat messages." data-i18n="[title]Disallow embedded media from other domains in chat messages">
|
||||
<input id="forbid_external_media" type="checkbox" />
|
||||
<small data-i18n="Forbid External Media">Forbid External Media</small>
|
||||
</label>
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "الإعدادات المسبقة لـ Kobold",
|
||||
"guikoboldaisettings": "إعدادات واجهة KoboldAI",
|
||||
"novelaipreserts": "الإعدادات المسبقة لـ NovelAI",
|
||||
"default": "افتراضي",
|
||||
"openaipresets": "الإعدادات المسبقة لـ OpenAI",
|
||||
"text gen webio(ooba) presets": "الإعدادات المسبقة لـ WebUI(ooba)",
|
||||
"response legth(tokens)": "طول الاستجابة (بعدد الاحرف او الرموز)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "سرد العالم أولاً",
|
||||
"Recursive Scan": "فحص متكرر",
|
||||
"Case Sensitive": "حساس لحالة الأحرف",
|
||||
"Match whole words": "تطابق الكلمات الكاملة",
|
||||
"Alert On Overflow": "تنبيه عند التجاوز",
|
||||
"World/Lore Editor": "محرر العالم/السرد",
|
||||
"--- None ---": "--- لا شيء ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold-Einstellungen von vorher",
|
||||
"guikoboldaisettings": "KoboldAI-Einstellungen für das Menü",
|
||||
"novelaipreserts": "NovelAI-Einstellungen von früher",
|
||||
"default": "Normal",
|
||||
"openaipresets": "OpenAI-Einstellungen von vorher",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-Einstellungen für Texterstellung",
|
||||
"response legth(tokens)": "Länge der Antwort (Tokens)",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Globale Lore zuerst",
|
||||
"Recursive Scan": "Rekursive Suche",
|
||||
"Case Sensitive": "Groß-/Kleinschreibung beachten",
|
||||
"Match whole words": "Ganze Wörter abgleichen",
|
||||
"Alert On Overflow": "Warnung bei Überlauf",
|
||||
"World/Lore Editor": "Welt-/Lore-Editor",
|
||||
"--- None ---": "--- Keine ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Preajustes de Kobold",
|
||||
"guikoboldaisettings": "Ajustes de interfaz de KoboldAI",
|
||||
"novelaipreserts": "Preajustes de NovelAI",
|
||||
"default": "Predeterminado",
|
||||
"openaipresets": "Preajustes de OpenAI",
|
||||
"text gen webio(ooba) presets": "Preajustes de Text Gen WebUI(ooba)",
|
||||
"response legth(tokens)": "Longitud de respuesta (tokens)",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Historia Global Primero",
|
||||
"Recursive Scan": "Escaneo Recursiva",
|
||||
"Case Sensitive": "Sensible a mayúsculas y minúsculas",
|
||||
"Match whole words": "Coincidir palabras completas",
|
||||
"Alert On Overflow": "Alerta en Desbordamiento",
|
||||
"World/Lore Editor": "Editor de Mundo/Historia",
|
||||
"--- None ---": "--- Ninguno ---",
|
||||
@ -891,6 +889,7 @@
|
||||
"Chat API": " API de chat",
|
||||
"and pick a character": "y elige un personaje",
|
||||
"in the chat bar": "en la barra de chat",
|
||||
"You can browse a list of bundled characters in the Download Extensions & Assets menu within": "Puedes explorar una lista de personajes incluidos en el menú de Download Extensions & Assets dentro de ",
|
||||
"Confused or lost?": "¿Confundido o perdido?",
|
||||
"click these icons!": "¡Haz clic en estos iconos!",
|
||||
"SillyTavern Documentation Site": "Sitio de documentación de SillyTavern",
|
||||
@ -914,7 +913,4 @@
|
||||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Usa el tokenizador apropiado para los modelos de Google a través de su API. Procesamiento de indicaciones más lento, pero ofrece un recuento de tokens mucho más preciso.",
|
||||
"Load koboldcpp order": "Cargar orden de koboldcpp",
|
||||
"Use Google Tokenizer": "Usar Tokenizador de Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Préréglages de Kobold",
|
||||
"guikoboldaisettings": "Paramètres de l'interface utilisateur de KoboldAI",
|
||||
"novelaipreserts": "Préréglages de NovelAI",
|
||||
"default": "Par défaut",
|
||||
"openaipresets": "Préréglages d'OpenAI",
|
||||
"text gen webio(ooba) presets": "Préréglages de WebUI(ooba)",
|
||||
"response legth(tokens)": "Longueur de la réponse (en tokens)",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "Lore global d'abord",
|
||||
"Recursive Scan": "Analyse récursive",
|
||||
"Case Sensitive": "Sensible à la casse",
|
||||
"Match whole words": "Correspondre aux mots entiers",
|
||||
"Alert On Overflow": "Alerte en cas de dépassement",
|
||||
"World/Lore Editor": "Éditeur de monde/lore",
|
||||
"--- None ---": "--- Aucun ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Fyrir stillingar Kobold",
|
||||
"guikoboldaisettings": "Stillingar fyrir KoboldAI viðmót",
|
||||
"novelaipreserts": "Fyrir stillingar NovelAI",
|
||||
"default": "Sjálfgefið",
|
||||
"openaipresets": "Fyrir stillingar OpenAI",
|
||||
"text gen webio(ooba) presets": "Fyrir stillingar WebUI(ooba) textagerðar",
|
||||
"response legth(tokens)": "Lengd svars (í táknum eða stöfum)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Fyrst heimsfræði",
|
||||
"Recursive Scan": "Endurkvæm skoðun",
|
||||
"Case Sensitive": "Skilgreiningarfræðilegt",
|
||||
"Match whole words": "Nákvæm samræmi",
|
||||
"Alert On Overflow": "Viðvörun um flæði",
|
||||
"World/Lore Editor": "Heims-/fræðiritari",
|
||||
"--- None ---": "--- Engin ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Preimpostazioni Kobold",
|
||||
"guikoboldaisettings": "Impostazioni dell'interfaccia KoboldAI",
|
||||
"novelaipreserts": "Preimpostazioni NovelAI",
|
||||
"default": "Predefinito",
|
||||
"openaipresets": "Preimpostazioni OpenAI",
|
||||
"text gen webio(ooba) presets": "Preimpostazioni WebUI(ooba) per la generazione di testo",
|
||||
"response legth(tokens)": "Lunghezza della risposta (token)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Lore Globale Prima",
|
||||
"Recursive Scan": "Scansione Ricorsiva",
|
||||
"Case Sensitive": "Sensibile alle Maiuscole",
|
||||
"Match whole words": "Corrispondi a parole intere",
|
||||
"Alert On Overflow": "Avviso Su Overflow",
|
||||
"World/Lore Editor": "Editor di Mondo/Lore",
|
||||
"--- None ---": "--- Nessuno ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Koboldのプリセット",
|
||||
"guikoboldaisettings": "KoboldAIのGUI設定",
|
||||
"novelaipreserts": "NovelAIのプリセット",
|
||||
"default": "デフォルト",
|
||||
"openaipresets": "OpenAIのプリセット",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)のプリセット",
|
||||
"response legth(tokens)": "応答の長さ(トークン数)",
|
||||
@ -494,7 +493,6 @@
|
||||
"Global Lore First": "グローバルロアを最初に表示",
|
||||
"Recursive Scan": "再帰的スキャン",
|
||||
"Case Sensitive": "大文字と小文字を区別する",
|
||||
"Match whole words": "完全な単語の一致",
|
||||
"Alert On Overflow": "オーバーフロー時に警告",
|
||||
"World/Lore Editor": "ワールド/ロアの編集",
|
||||
"--- None ---": "--- なし ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "코볼드 사전 설정",
|
||||
"guikoboldaisettings": "KoboldAI 인터페이스 설정",
|
||||
"novelaipreserts": "NovelAI 사전 설정",
|
||||
"default": "기본값",
|
||||
"openaipresets": "OpenAI 사전 설정",
|
||||
"text gen webio(ooba) presets": "텍스트 생성 WebUI(ooba) 사전 설정",
|
||||
"response legth(tokens)": "응답 길이 (토큰)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "글로벌 로어 우선",
|
||||
"Recursive Scan": "재귀 스캔",
|
||||
"Case Sensitive": "대소문자 구분",
|
||||
"Match whole words": "전체 단어 일치",
|
||||
"Alert On Overflow": "오버플로우 알림",
|
||||
"World/Lore Editor": "월드/로어 편집기",
|
||||
"--- None ---": "--- 없음 ---",
|
||||
|
@ -1,6 +1,7 @@
|
||||
[
|
||||
{ "lang": "ar-sa", "display": "عربي (Arabic)" },
|
||||
{ "lang": "zh-cn", "display": "简体中文 (Chinese) (Simplified)" },
|
||||
{ "lang": "zh-tw", "display": "繁體中文 (Chinese) (Taiwan)" },
|
||||
{ "lang": "nl-nl", "display": "Nederlands (Dutch)" },
|
||||
{ "lang": "de-de", "display": "Deutsch (German)" },
|
||||
{ "lang": "fr-fr", "display": "Français (French)" },
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold voorinstellingen",
|
||||
"guikoboldaisettings": "KoboldAI-interface-instellingen",
|
||||
"novelaipreserts": "NovelAI-voorinstellingen",
|
||||
"default": "Standaard",
|
||||
"openaipresets": "OpenAI-voorinstellingen",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-voorinstellingen voor tekstgeneratie",
|
||||
"response legth(tokens)": "Reactielengte (tokens)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Globale Lore Eerst",
|
||||
"Recursive Scan": "Recursieve Scan",
|
||||
"Case Sensitive": "Hoofdlettergevoelig",
|
||||
"Match whole words": "Hele woorden matchen",
|
||||
"Alert On Overflow": "Waarschuwing bij overloop",
|
||||
"World/Lore Editor": "Wereld/Lore Editor",
|
||||
"--- None ---": "--- Geen ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Configurações predefinidas do Kobold",
|
||||
"guikoboldaisettings": "Configurações da interface do KoboldAI",
|
||||
"novelaipreserts": "Configurações predefinidas do NovelAI",
|
||||
"default": "Padrão",
|
||||
"openaipresets": "Configurações predefinidas do OpenAI",
|
||||
"text gen webio(ooba) presets": "Configurações predefinidas do WebUI(ooba) para geração de texto",
|
||||
"response legth(tokens)": "Comprimento da resposta (tokens)",
|
||||
@ -493,7 +492,6 @@
|
||||
"Global Lore First": "Lore Global Primeiro",
|
||||
"Recursive Scan": "Verificação Recursiva",
|
||||
"Case Sensitive": "Sensível a Maiúsculas",
|
||||
"Match whole words": "Corresponder palavras inteiras",
|
||||
"Alert On Overflow": "Alerta em Overflow",
|
||||
"World/Lore Editor": "Editor de Mundo/Lore",
|
||||
"--- None ---": "--- Nenhum ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Пресеты для Kobold",
|
||||
"guikoboldaisettings": "Настройки из интерфейса KoboldAI",
|
||||
"novelaipreserts": "Пресеты для NovelAI",
|
||||
"default": "По умолчанию",
|
||||
"openaipresets": "Пресеты для OpenAI",
|
||||
"text gen webio(ooba) presets": "Пресеты для WebUI(ooba)",
|
||||
"response legth(tokens)": "Ответ (в токенах)",
|
||||
@ -276,7 +275,7 @@
|
||||
"World Info": "Информация о мире",
|
||||
"Scan Depth": "Глубина сканирования",
|
||||
"Case-Sensitive": "С учетом регистра",
|
||||
"Match Whole Words": "Только целые слова",
|
||||
"Match Whole Words": "Только полное совпадение",
|
||||
"Use global setting": "Использовать глобальную настройку",
|
||||
"Yes": "Да",
|
||||
"No": "Нет",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Сначала глобальный лор",
|
||||
"Recursive Scan": "Рекурсивное сканирование",
|
||||
"Case Sensitive": "Учитывать регистр",
|
||||
"Match whole words": "Только полное совпадение",
|
||||
"Alert On Overflow": "Оповещение о переполнении",
|
||||
"World/Lore Editor": "Редактировать мир или лор",
|
||||
"--- None ---": "--- Отсутствует ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Налаштування Kobold",
|
||||
"guikoboldaisettings": "З інтерфейсу KoboldAI",
|
||||
"novelaipreserts": "Налаштування NovelAI",
|
||||
"default": "За замовчуванням",
|
||||
"openaipresets": "Налаштування OpenAI",
|
||||
"text gen webio(ooba) presets": "Налаштування Text Completion",
|
||||
"response legth(tokens)": "Відповідь (токени)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Глобальна інформація першою",
|
||||
"Recursive Scan": "Рекурсивне сканування",
|
||||
"Case Sensitive": "Чутливість до регістру",
|
||||
"Match whole words": "Відповідність цілим словам",
|
||||
"Alert On Overflow": "Сповіщення при переповненні",
|
||||
"World/Lore Editor": "Редактор світу/книги",
|
||||
"--- None ---": "--- Нічого ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Cài đặt trước Kobold",
|
||||
"guikoboldaisettings": "Cài đặt giao diện KoboldAI",
|
||||
"novelaipreserts": "Cài đặt trước NovelAI",
|
||||
"default": "Mặc định",
|
||||
"openaipresets": "Cài đặt trước OpenAI",
|
||||
"text gen webio(ooba) presets": "Cài đặt trước WebUI(ooba) của máy tạo văn bản",
|
||||
"response legth(tokens)": "Độ dài phản hồi (trong các token)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "Sử liệu toàn cầu đầu tiên",
|
||||
"Recursive Scan": "Quét đệ quy",
|
||||
"Case Sensitive": "Phân biệt chữ hoa chữ thường",
|
||||
"Match whole words": "Khớp toàn bộ từ",
|
||||
"Alert On Overflow": "Cảnh báo khi tràn",
|
||||
"World/Lore Editor": "Trình soạn thảo Thế giới/Sử liệu",
|
||||
"--- None ---": "--- Không ---",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"kobldpresets": "Kobold 预设",
|
||||
"guikoboldaisettings": "KoboldAI 用户界面设置",
|
||||
"novelaipreserts": "NovelAI 预设",
|
||||
"default": "默认",
|
||||
"openaipresets": "对话补全预设",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba) 预设",
|
||||
"response legth(tokens)": "响应长度(以词符数计)",
|
||||
@ -495,7 +494,6 @@
|
||||
"Global Lore First": "全局世界书优先",
|
||||
"Recursive Scan": "递归扫描",
|
||||
"Case Sensitive": "区分大小写",
|
||||
"Match whole words": "完整匹配单词",
|
||||
"Alert On Overflow": "溢出警报",
|
||||
"World/Lore Editor": "世界书编辑器",
|
||||
"--- None ---": "--- 无 ---",
|
||||
|
1192
public/locales/zh-tw.json
Normal file
1192
public/locales/zh-tw.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -8636,11 +8636,6 @@ jQuery(async function () {
|
||||
callback: doCloseChat,
|
||||
helpString: 'Closes the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'closechat',
|
||||
callback: doCloseChat,
|
||||
helpString: 'Closes the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'panels',
|
||||
callback: doTogglePanels,
|
||||
aliases: ['togglepanels'],
|
||||
|
@ -4,7 +4,7 @@
|
||||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
|
@ -7,7 +7,7 @@
|
||||
Don't include the page name!
|
||||
</i>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://streetcat.wiki/index.php</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>https://tcrf.net</code>
|
||||
|
@ -1020,12 +1020,12 @@ function parseLlmResponse(emotionResponse, labels) {
|
||||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
} catch {
|
||||
const fuse = new Fuse([emotionResponse]);
|
||||
for (const label of labels) {
|
||||
const result = fuse.search(label);
|
||||
const fuse = new Fuse(labels, { includeScore: true });
|
||||
console.debug('Using fuzzy search in labels:', labels);
|
||||
const result = fuse.search(emotionResponse);
|
||||
if (result.length > 0) {
|
||||
return label;
|
||||
}
|
||||
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
|
||||
return result[0].item;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,6 +342,16 @@ export class QuickReply {
|
||||
message.addEventListener('scroll', (evt)=>{
|
||||
updateScrollDebounced();
|
||||
});
|
||||
/** @type {any} */
|
||||
const resizeListener = debounce((evt) => {
|
||||
updateSyntax();
|
||||
updateScrollDebounced(evt);
|
||||
if (document.activeElement == message) {
|
||||
message.blur();
|
||||
message.focus();
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', resizeListener);
|
||||
message.style.color = 'transparent';
|
||||
message.style.background = 'transparent';
|
||||
message.style.setProperty('text-shadow', 'none', 'important');
|
||||
@ -514,6 +524,8 @@ export class QuickReply {
|
||||
});
|
||||
|
||||
await popupResult;
|
||||
|
||||
window.removeEventListener('resize', resizeListener);
|
||||
} else {
|
||||
warn('failed to fetch qrEditor template');
|
||||
}
|
||||
|
@ -266,6 +266,7 @@ const default_settings = {
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
@ -342,6 +343,7 @@ const oai_settings = {
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
@ -1767,7 +1769,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
|
||||
// Don't add a prefill on quiet gens (summarization)
|
||||
if (!isQuiet) {
|
||||
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
|
||||
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2760,6 +2762,7 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
|
||||
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
|
||||
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
|
||||
oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation;
|
||||
oai_settings.human_sysprompt_message = settings.human_sysprompt_message ?? default_settings.human_sysprompt_message;
|
||||
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
|
||||
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
|
||||
@ -2796,6 +2799,7 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#api_url_scale').val(oai_settings.api_url_scale);
|
||||
$('#openai_proxy_password').val(oai_settings.proxy_password);
|
||||
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
|
||||
$('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation);
|
||||
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
|
||||
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
|
||||
$('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check);
|
||||
@ -3115,6 +3119,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
api_url_scale: settings.api_url_scale,
|
||||
show_external_models: settings.show_external_models,
|
||||
assistant_prefill: settings.assistant_prefill,
|
||||
assistant_impersonation: settings.assistant_impersonation,
|
||||
human_sysprompt_message: settings.human_sysprompt_message,
|
||||
use_ai21_tokenizer: settings.use_ai21_tokenizer,
|
||||
use_google_tokenizer: settings.use_google_tokenizer,
|
||||
@ -3501,6 +3506,7 @@ function onSettingsPresetChange() {
|
||||
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
|
||||
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
|
||||
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
|
||||
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false],
|
||||
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
|
||||
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
|
||||
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
|
||||
@ -3526,6 +3532,11 @@ function onSettingsPresetChange() {
|
||||
preset.names_behavior = character_names_behavior.COMPLETION;
|
||||
}
|
||||
|
||||
// Claude: Assistant Impersonation Prefill = Inherit from Assistant Prefill
|
||||
if (preset.assistant_prefill !== undefined && preset.assistant_impersonation === undefined) {
|
||||
preset.assistant_impersonation = preset.assistant_prefill;
|
||||
}
|
||||
|
||||
const updateInput = (selector, value) => $(selector).val(value).trigger('input');
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
|
||||
|
||||
@ -4721,6 +4732,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_assistant_impersonation').on('input', function () {
|
||||
oai_settings.assistant_impersonation = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_human_sysprompt_textarea').on('input', function () {
|
||||
oai_settings.human_sysprompt_message = String($('#claude_human_sysprompt_textarea').val());
|
||||
saveSettingsDebounced();
|
||||
|
@ -114,7 +114,9 @@ class PresetManager {
|
||||
* @returns {any} Preset value
|
||||
*/
|
||||
findPreset(name) {
|
||||
return $(this.select).find(`option:contains(${name})`).val();
|
||||
return $(this.select).find('option').filter(function() {
|
||||
return $(this).text() === name;
|
||||
}).val();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -447,8 +447,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unhide',
|
||||
],
|
||||
helpString: 'Unhides a message from the prompt.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'disable',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-disable',
|
||||
callback: disableGroupMemberCallback,
|
||||
aliases: ['disable', 'disablemember', 'memberdisable'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -456,7 +457,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'disable',
|
||||
],
|
||||
helpString: 'Disables a group member from being drafted for replies.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'enable',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-enable',
|
||||
aliases: ['enable', 'enablemember', 'memberenable'],
|
||||
callback: enableGroupMemberCallback,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
@ -465,9 +467,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'enable',
|
||||
],
|
||||
helpString: 'Enables a group member to be drafted for replies.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberadd',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-add',
|
||||
callback: addGroupMemberCallback,
|
||||
aliases: ['addmember'],
|
||||
aliases: ['addmember', 'memberadd'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'character name', [ARGUMENT_TYPE.STRING], true,
|
||||
@ -481,15 +483,15 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberadd',
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/memberadd John Doe</code></pre>
|
||||
<pre><code>/member-add John Doe</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberremove',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-remove',
|
||||
callback: removeGroupMemberCallback,
|
||||
aliases: ['removemember'],
|
||||
aliases: ['removemember', 'memberremove'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -503,16 +505,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberremove
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/memberremove 2</code></pre>
|
||||
<pre><code>/memberremove John Doe</code></pre>
|
||||
<pre><code>/member-remove 2</code></pre>
|
||||
<pre><code>/member-remove John Doe</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-up',
|
||||
callback: moveGroupMemberUpCallback,
|
||||
aliases: ['upmember'],
|
||||
aliases: ['upmember', 'memberup'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -520,9 +522,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup',
|
||||
],
|
||||
helpString: 'Moves a group member up in the group chat list.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberdown',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-down',
|
||||
callback: moveGroupMemberDownCallback,
|
||||
aliases: ['downmember'],
|
||||
aliases: ['downmember', 'memberdown'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@ -2766,10 +2768,12 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
|
||||
}
|
||||
} catch (e) {
|
||||
document.querySelector('#form_sheld').classList.add('script_error');
|
||||
toastr.error(e.message);
|
||||
result = new SlashCommandClosureResult();
|
||||
result.isError = true;
|
||||
result.errorMessage = e.message;
|
||||
if (e.cause !== 'abort') {
|
||||
toastr.error(e.message);
|
||||
}
|
||||
} finally {
|
||||
delay(1000).then(()=>clearCommandProgressDebounced());
|
||||
|
||||
|
@ -58,6 +58,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
return new RegExp('=(.*)');
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(this.executor.command?.namedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name));
|
||||
let name;
|
||||
let value;
|
||||
@ -130,6 +133,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
}
|
||||
|
||||
getUnnamedArgumentAt(text, index, isSelect) {
|
||||
if (!Array.isArray(this.executor.command?.unnamedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const lastArgIsBlank = this.executor.unnamedArgumentList.slice(-1)[0]?.value == '';
|
||||
const notProvidedArguments = this.executor.command.unnamedArgumentList.slice(this.executor.unnamedArgumentList.length - (lastArgIsBlank ? 1 : 0));
|
||||
let value;
|
||||
|
@ -14,10 +14,12 @@ import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
|
||||
export {
|
||||
TAG_FOLDER_TYPES,
|
||||
@ -357,7 +359,7 @@ function createTagMapFromList(listElement, key) {
|
||||
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
|
||||
*
|
||||
* @param {string} key - The key for which to get tags via the tag map
|
||||
* @param {boolean} [sort=true] -
|
||||
* @param {boolean} [sort=true] - Whether the tag list should be sorted
|
||||
* @returns {Tag[]} A list of tags
|
||||
*/
|
||||
function getTagsList(key, sort = true) {
|
||||
@ -463,35 +465,122 @@ export function getTagKeyForEntityElement(element) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag to a given entity
|
||||
* @param {Tag} tag - The tag to add
|
||||
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
|
||||
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
|
||||
* @returns {boolean} Whether at least one tag was added
|
||||
*/
|
||||
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
|
||||
let result = false;
|
||||
// Add tags to the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = addTagToMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
if (tagListSelector) printTagList(tagListSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given entity
|
||||
* @param {Tag} tag - The tag to remove
|
||||
* @param {string|string[]} entityId - The entity to remove this tag from. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the tag removed too (for example because the add was triggered for that function)
|
||||
* @param {JQuery<HTMLElement>?} [options.tagElement=null] - Optionally a direct html element of the tag to be removed, so it can be removed from the UI
|
||||
* @returns {boolean} Whether at least one tag was removed
|
||||
*/
|
||||
export function removeTagFromEntity(tag, entityId, { tagListSelector = null, tagElement = null } = {}) {
|
||||
let result = false;
|
||||
// Remove tag from the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = removeTagFromMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = removeTagFromMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We don't reprint the lists, we can just remove the html elements from them.
|
||||
if (tagListSelector) {
|
||||
const $selector = (typeof tagListSelector === 'string') ? $(tagListSelector) : tagListSelector;
|
||||
$selector.find(`.tag[id="${tag.id}"]`).remove();
|
||||
}
|
||||
if (tagElement) tagElement.remove();
|
||||
$(`${getInlineListSelector()} .tag[id="${tag.id}"]`).remove();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag from a given character. If no character is provided, adds it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was added or not
|
||||
*/
|
||||
function addTagToMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [tagId];
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (tag_map[key].includes(tagId))
|
||||
return false;
|
||||
|
||||
tag_map[key].push(tagId);
|
||||
tag_map[key] = tag_map[key].filter(onlyUnique);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given character. If no character is provided, removes it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was removed or not
|
||||
*/
|
||||
function removeTagFromMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [];
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
const indexOf = tag_map[key].indexOf(tagId);
|
||||
tag_map[key].splice(indexOf, 1);
|
||||
return indexOf !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -535,24 +624,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
|
||||
} else {
|
||||
addTagToMap(tag.id);
|
||||
}
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
printTagList(listSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
@ -635,6 +707,7 @@ function createNewTag(tagName) {
|
||||
create_date: Date.now(),
|
||||
};
|
||||
tags.push(tag);
|
||||
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@ -932,8 +1005,8 @@ function updateTagFilterIndicator() {
|
||||
|
||||
function onTagRemoveClick(event) {
|
||||
event.stopPropagation();
|
||||
const tag = $(this).closest('.tag');
|
||||
const tagId = tag.attr('id');
|
||||
const tagElement = $(this).closest('.tag');
|
||||
const tagId = tagElement.attr('id');
|
||||
|
||||
// Check if we are inside the drilldown. If so, we call remove on the bogus folder
|
||||
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
|
||||
@ -942,24 +1015,13 @@ function onTagRemoveClick(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = tags.find(t => t.id === tagId);
|
||||
|
||||
// Optional, check for multiple character ids being present.
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
tag.remove();
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
|
||||
} else {
|
||||
removeTagFromMap(tagId);
|
||||
}
|
||||
|
||||
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
|
||||
removeTagFromEntity(tag, characterIds, { tagElement: tagElement });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
@ -985,7 +1047,7 @@ function onGroupCreateClick() {
|
||||
|
||||
export function applyTagsOnCharacterSelect() {
|
||||
//clearTagsFilter();
|
||||
const chid = Number($(this).attr('chid'));
|
||||
const chid = Number(this_chid);
|
||||
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
@ -1461,14 +1523,200 @@ function printViewTagList(empty = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerTagsSlashCommands() {
|
||||
/**
|
||||
* Gets the key for char/group for a slash command. If none can be found, a toastr will be shown and null returned.
|
||||
* @param {string?} [charName] The optionally provided char name
|
||||
* @returns {string?} - The char/group key, or null if none found
|
||||
*/
|
||||
function paraGetCharKey(charName) {
|
||||
const entity = charName
|
||||
? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
|
||||
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
|
||||
const key = getTagKeyForEntity(entity);
|
||||
if (!key) {
|
||||
toastr.warning(`Character ${charName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
/**
|
||||
* Gets a tag by its name. Optionally can create the tag if it does not exist.
|
||||
* @param {string} tagName - The name of the tag
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean} [options.allowCreate=false] - Whether a new tag should be created if no tag with the name exists
|
||||
* @returns {Tag?} The tag, or null if not found
|
||||
*/
|
||||
function paraGetTag(tagName, { allowCreate = false } = {}) {
|
||||
if (!tagName) {
|
||||
toastr.warning('Tag name must be provided.');
|
||||
return null;
|
||||
}
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
if (allowCreate && !tag) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
if (!tag) {
|
||||
toastr.warning(`Tag ${tagName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
function updateTagsList() {
|
||||
switch (menu_type) {
|
||||
case 'characters':
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
break;
|
||||
case 'character_edit':
|
||||
applyTagsOnCharacterSelect();
|
||||
break;
|
||||
case 'group_edit':
|
||||
select_group_chats(selected_group, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-add',
|
||||
returns: 'true/false - Whether the tag was added or was assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName, { allowCreate: true });
|
||||
if (!tag) return 'false';
|
||||
const result = addTagToEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Adds a tag to the character. If no character is provided, it adds it to the current character (<code>{{char}}</code>).
|
||||
If the tag doesn't exist, it is created.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-add name="Chloe" scenario</code></pre>
|
||||
will add the tag "scenario" to the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-remove',
|
||||
returns: 'true/false - Whether the tag was removed or wasn\'t assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
const result = removeTagFromEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Removes a tag from the character. If no character is provided, it removes it from the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-remove name="Chloe" scenario</code></pre>
|
||||
will remove the tag "scenario" from the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-exists',
|
||||
returns: 'true/false - Whether the given tag name is assigned to the character',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
return String(tag_map[key].includes(tag.id));
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Checks whether the given tag is assigned to the character. If no character is provided, it checks the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-exists name="Chloe" scenario</code></pre>
|
||||
will return true if the character named Chloe has the tag "scenario".
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-list',
|
||||
returns: 'Comma-separated list of all assigned tags',
|
||||
/** @param {{name: string}} namedArgs @returns {string} */
|
||||
callback: ({ name }) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return '';
|
||||
const tags = getTagsList(key);
|
||||
return tags.map(x => x.name).join(', ');
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Lists all assigned tags of the character. If no character is provided, it uses the current character (<code>{{char}}</code>).
|
||||
<br />
|
||||
Note that there is no special handling for tags containing commas, they will be printed as-is.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-list name="Chloe"</code></pre>
|
||||
could return something like <code>OC, scenario, edited, funny</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
}
|
||||
|
||||
export function initTags() {
|
||||
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
||||
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
||||
|
||||
$(document).on('click', '#rm_button_create', onCharacterCreateClick);
|
||||
$(document).on('click', '#rm_button_group_chats', onGroupCreateClick);
|
||||
$(document).on('click', '.character_select', applyTagsOnCharacterSelect);
|
||||
$(document).on('click', '.group_select', applyTagsOnGroupSelect);
|
||||
$(document).on('click', '.tag_remove', onTagRemoveClick);
|
||||
$(document).on('input', '.tag_input', onTagInput);
|
||||
$(document).on('click', '.tags_view', onViewTagsListClick);
|
||||
@ -1479,6 +1727,7 @@ export function initTags() {
|
||||
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
|
||||
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
|
||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
||||
|
||||
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
|
||||
const toggle = $(evt.target).is(':checked');
|
||||
@ -1506,4 +1755,6 @@ export function initTags() {
|
||||
printCharactersDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
registerTagsSlashCommands();
|
||||
}
|
||||
|
@ -11,9 +11,10 @@
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-plug"></i></code><span data-i18n="and select a"> and select a </span><a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank" data-i18n="Chat API">Chat API</a>.</span>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character"> and pick a character</span>
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character."> and pick a character.</span>
|
||||
</li>
|
||||
</ol>
|
||||
<span data-i18n="You can browse a list of bundled characters in the Download Extensions & Assets menu within">You can browse a list of bundled characters in the <i>Download Extensions & Assets</i> menu within </span><code><i class="fa-solid fa-cubes"></i></code><span>.</span>
|
||||
<hr>
|
||||
<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
|
||||
<ul>
|
||||
|
@ -991,7 +991,7 @@ export function getTextGenModel() {
|
||||
}
|
||||
|
||||
export function isJsonSchemaSupported() {
|
||||
return settings.type === TABBY && main_api === 'textgenerationwebui';
|
||||
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
|
||||
}
|
||||
|
||||
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
|
||||
@ -1065,7 +1065,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
|
||||
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
|
||||
'grammar_string': settings.grammar_string,
|
||||
'json_schema': settings.type === TABBY ? settings.json_schema : undefined,
|
||||
'json_schema': [TABBY, LLAMACPP].includes(settings.type) ? settings.json_schema : undefined,
|
||||
// llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API
|
||||
'repeat_penalty': settings.rep_pen,
|
||||
'tfs_z': settings.tfs,
|
||||
@ -1150,5 +1150,15 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
|
||||
eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
|
||||
|
||||
// Grammar conflicts with with json_schema
|
||||
if (settings.type === LLAMACPP) {
|
||||
if (params.json_schema && Object.keys(params.json_schema).length > 0) {
|
||||
delete params.grammar_string;
|
||||
delete params.grammar;
|
||||
} else {
|
||||
delete params.json_schema;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe } from './utils.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean } from './utils.js';
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
@ -548,6 +548,19 @@ function registerWorldInfoSlashCommands() {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof newEntryTemplate[field] === 'boolean') {
|
||||
const isTrue = isTrueBoolean(value);
|
||||
const isFalse = isFalseBoolean(value);
|
||||
|
||||
if (isTrue) {
|
||||
value = String(true);
|
||||
}
|
||||
|
||||
if (isFalse) {
|
||||
value = String(false);
|
||||
}
|
||||
}
|
||||
|
||||
const fuse = new Fuse(entries, {
|
||||
keys: [{ name: field, weight: 1 }],
|
||||
includeScore: true,
|
||||
@ -1244,6 +1257,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
|
||||
}
|
||||
|
||||
worldEntriesList.sortable({
|
||||
items: '.world_entry',
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: async function (_event, _ui) {
|
||||
|
@ -2068,6 +2068,7 @@ input[type="file"] {
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bulk_select_checkbox {
|
||||
|
@ -7,7 +7,9 @@ const { getConfigValue, color } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const contentDirectory = path.join(process.cwd(), 'default/content');
|
||||
const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');
|
||||
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
||||
const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
|
||||
const characterCardParser = require('../character-card-parser.js');
|
||||
|
||||
const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
|
||||
@ -16,6 +18,8 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo
|
||||
* @typedef {Object} ContentItem
|
||||
* @property {string} filename
|
||||
* @property {string} type
|
||||
* @property {string} [name]
|
||||
* @property {string|null} [folder]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -48,9 +52,7 @@ const CONTENT_TYPES = {
|
||||
*/
|
||||
function getDefaultPresets(directories) {
|
||||
try {
|
||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||
const contentIndex = JSON.parse(contentIndexText);
|
||||
|
||||
const contentIndex = getContentIndex();
|
||||
const presets = [];
|
||||
|
||||
for (const contentItem of contentIndex) {
|
||||
@ -112,8 +114,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
|
||||
continue;
|
||||
}
|
||||
|
||||
contentLog.push(contentItem.filename);
|
||||
const contentPath = path.join(contentDirectory, contentItem.filename);
|
||||
if (!contentItem.folder) {
|
||||
console.log(`Content file ${contentItem.filename} has no parent folder`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentPath = path.join(contentItem.folder, contentItem.filename);
|
||||
|
||||
if (!fs.existsSync(contentPath)) {
|
||||
console.log(`Content file ${contentItem.filename} is missing`);
|
||||
@ -129,6 +135,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
|
||||
|
||||
const basePath = path.parse(contentItem.filename).base;
|
||||
const targetPath = path.join(contentTarget, basePath);
|
||||
contentLog.push(contentItem.filename);
|
||||
|
||||
if (fs.existsSync(targetPath)) {
|
||||
console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
|
||||
@ -157,8 +164,7 @@ async function checkForNewContent(directoriesList, forceCategories = []) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||
const contentIndex = JSON.parse(contentIndexText);
|
||||
const contentIndex = getContentIndex();
|
||||
let anyContentAdded = false;
|
||||
|
||||
for (const directories of directoriesList) {
|
||||
@ -179,6 +185,38 @@ async function checkForNewContent(directoriesList, forceCategories = []) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets combined content index from the content and scaffold directories.
|
||||
* @returns {ContentItem[]} Array of content index
|
||||
*/
|
||||
function getContentIndex() {
|
||||
const result = [];
|
||||
|
||||
if (fs.existsSync(scaffoldIndexPath)) {
|
||||
const scaffoldIndexText = fs.readFileSync(scaffoldIndexPath, 'utf8');
|
||||
const scaffoldIndex = JSON.parse(scaffoldIndexText);
|
||||
if (Array.isArray(scaffoldIndex)) {
|
||||
scaffoldIndex.forEach((item) => {
|
||||
item.folder = scaffoldDirectory;
|
||||
});
|
||||
result.push(...scaffoldIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(contentIndexPath)) {
|
||||
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8');
|
||||
const contentIndex = JSON.parse(contentIndexText);
|
||||
if (Array.isArray(contentIndex)) {
|
||||
contentIndex.forEach((item) => {
|
||||
item.folder = contentDirectory;
|
||||
});
|
||||
result.push(...contentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target directory for the specified asset type.
|
||||
* @param {ContentType} type Asset type
|
||||
|
Reference in New Issue
Block a user