mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-02 19:07:40 +01:00
Merge branch 'staging' into parser-followup-2
This commit is contained in:
commit
058f45ec71
@ -8,3 +8,5 @@ Start.bat
|
||||
cloudflared.exe
|
||||
access.log
|
||||
/data
|
||||
/cache
|
||||
.DS_Store
|
||||
|
@ -5,4 +5,6 @@ node_modules/
|
||||
secrets.json
|
||||
/dist
|
||||
/backups/
|
||||
/data
|
||||
/cache
|
||||
access.log
|
||||
|
@ -36,7 +36,6 @@ label[for="extensions_autoconnect"] {
|
||||
|
||||
.extensions_info {
|
||||
text-align: left;
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
.extensions_info h3 {
|
||||
|
@ -115,12 +115,10 @@ dialog {
|
||||
background-color: var(--crimson-hover);
|
||||
}
|
||||
|
||||
.menu_button.popup-button-custom {
|
||||
/* Custom buttons should not scale to smallest size, otherwise they will always break to multiline */
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.popup-controls .menu_button {
|
||||
/* Popup buttons should not scale to smallest size, otherwise they will always break to multiline if multiple words */
|
||||
width: unset;
|
||||
|
||||
/* Fix weird animation issue with fonts on brightness filter */
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0);
|
||||
|
@ -183,7 +183,6 @@
|
||||
.tag.selected {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
border: 1px solid lightgreen;
|
||||
}
|
||||
|
||||
.tag.excluded {
|
||||
|
6
public/img/manual.svg
Normal file
6
public/img/manual.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3252 3.05011L8.66765 20.4323L10.5995 20.9499L15.257 3.56775L13.3252 3.05011Z" />
|
||||
<path d="M7.61222 18.3608L8.97161 16.9124L8.9711 16.8933L3.87681 12.1121L8.66724 7.00798L7.20892 5.63928L1.0498 12.2017L7.61222 18.3608Z" />
|
||||
<path d="M16.3883 18.3608L15.0289 16.9124L15.0294 16.8933L20.1237 12.1121L15.3333 7.00798L16.7916 5.63928L22.9507 12.2017L16.3883 18.3608Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 514 B |
@ -4873,12 +4873,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- various fullscreen popups -->
|
||||
<template id="popup_template" data-i18n="[popup-button-save]popup-button-save;[popup-button-yes]popup-button-yes;[popup-button-no]popup-button-no;[popup-button-cancel]popup-button-cancel;[popup-button-import]popup-button-import" popup-button-save="Save" popup-button-yes="Yes" popup-button-no="No" popup-button-cancel="Cancel" popup-button-import="Import"> <!-- localization data holder for popups -->
|
||||
<template id="popup_template" data-i18n="[popup-button-save]popup-button-save;[popup-button-yes]popup-button-yes;[popup-button-no]popup-button-no;[popup-button-cancel]popup-button-cancel;[popup-button-import]popup-button-import;[popup-button-crop]popup-button-crop" popup-button-save="Save" popup-button-yes="Yes" popup-button-no="No" popup-button-cancel="Cancel" popup-button-import="Import" popup-button-crop="Crop"> <!-- localization data holder for popups -->
|
||||
<dialog class="popup">
|
||||
<div class="popup-body">
|
||||
<div class="popup-content">
|
||||
<h3 class="popup-header">text</h3>
|
||||
</div>
|
||||
<div class="popup-crop-wrap">
|
||||
<img class="popup-crop-image" src="">
|
||||
</div>
|
||||
<textarea class="popup-input text_pole result-control" rows="1" data-result="1" data-result-event="submit"></textarea>
|
||||
<div class="popup-controls">
|
||||
<div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
|
||||
@ -5422,7 +5425,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container wide100p flexGap10">
|
||||
<div class="flex3 flex-container flexFlowColumn flexNoGap">
|
||||
<div class="flex4 flex-container flexFlowColumn flexNoGap">
|
||||
<div class="flex-container justifySpaceBetween">
|
||||
<small for="group">
|
||||
<span data-i18n="Inclusion Group">Inclusion Group</span>
|
||||
@ -5444,7 +5447,7 @@
|
||||
<input type="text" class="text_pole margin0" name="group" rows="1" data-i18n="[placeholder]Only one entry with the same label will be activated" placeholder="Only one entry with the same label will be activated">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap" data-i18n="[title]A relative likelihood of entry activation within the group" title="A relative likelihood of entry activation within the group">
|
||||
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]A relative likelihood of entry activation within the group" title="A relative likelihood of entry activation within the group">
|
||||
<div class="flex-container justifySpaceBetween marginBot5">
|
||||
<small for="groupWeight" data-i18n="Group Weight">
|
||||
Group Weight
|
||||
@ -5480,6 +5483,19 @@
|
||||
<input class="text_pole margin0" name="cooldown" type="number" placeholder="No cooldown" min="0" max="999999">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a delay can't be activated until there are N messages present in the chat." title="Entries with a delay can't be activated until there are N messages present in the chat.">
|
||||
<div class="flex-container justifySpaceBetween marginBot5">
|
||||
<small class="flex-container alignItemsBaseline" for="delay" data-i18n="Delay">
|
||||
<span data-i18n="Delay">
|
||||
Delay
|
||||
</span>
|
||||
<i class="fa-solid fa-comments fa-xs"></i>
|
||||
</small>
|
||||
</div>
|
||||
<div class="range-block-range">
|
||||
<input class="text_pole margin0" name="delay" type="number" placeholder="No delay" min="0" max="999999">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container wide100p flexGap10">
|
||||
<div class="flex4 flex-container flexFlowColumn flexNoGap">
|
||||
@ -5905,32 +5921,7 @@
|
||||
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="16"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forbid_media_override_template" class="template_element">
|
||||
<div class="forbid_media_override flex-container flexFlowColumn">
|
||||
<h4 data-i18n="Forbid Media Override explanation" class="margin0">
|
||||
Ability of the current character/group to use external media in chats.
|
||||
</h4>
|
||||
<small data-i18n="Forbid Media Override subtitle" class="marginBot5">
|
||||
Media: images, videos, audio. External: not hosted on the local server.
|
||||
</small>
|
||||
<label class="checkbox_label" for="forbid_media_override_global">
|
||||
<input type="radio" id="forbid_media_override_global" name="forbid_media_override" />
|
||||
<span>
|
||||
<span data-i18n="Use global setting">Use global setting</span>
|
||||
<b class="forbid_media_global_state_forbidden">(forbidden)</b>
|
||||
<b class="forbid_media_global_state_allowed">(allowed)</b>
|
||||
</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_media_override_forbidden">
|
||||
<input type="radio" id="forbid_media_override_forbidden" name="forbid_media_override" />
|
||||
<span data-i18n="Always forbidden">Always forbidden</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_media_override_allowed">
|
||||
<input type="radio" id="forbid_media_override_allowed" name="forbid_media_override" />
|
||||
<span data-i18n="Always allowed">Always allowed</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- chat and input bar -->
|
||||
<div id="typing_indicator_template" class="template_element">
|
||||
<div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
|
||||
@ -6422,9 +6413,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div id="rawPromptPopup" class="list-group">
|
||||
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
|
||||
</div>
|
||||
<div id="user_avatar_template" class="template_element">
|
||||
<div class="avatar-container">
|
||||
<div imgfile="" class="avatar">
|
||||
|
@ -650,6 +650,11 @@
|
||||
"Char List Subheader": "角色列表子标题",
|
||||
"Character Version": "角色版本",
|
||||
"Created by": "创作者",
|
||||
"Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定义在导入卡片时应选择哪种操作来导入其列出的标签。“询问”将始终显示对话框。",
|
||||
"Import Card Tags": "导入卡片标签",
|
||||
"Ask": "询问",
|
||||
"All": "全部",
|
||||
"Existing": "现存的",
|
||||
"Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊匹配,在列表中通过所有数据字段搜索角色,而不仅仅是名称子字符串",
|
||||
"Advanced Character Search": "高级角色搜索",
|
||||
"If checked and the character card contains a prompt override (System Prompt), use that instead": "如果角色卡包含提示词,则使用它替代系统提示词",
|
||||
@ -660,8 +665,6 @@
|
||||
"Never resize avatars": "永不调整头像大小",
|
||||
"Show actual file names on the disk, in the characters list display only": "在角色列表显示中,显示磁盘上实际的文件名。",
|
||||
"Show avatar filenames": "显示头像文件名",
|
||||
"Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored": "在导入角色时导入嵌入式卡片标签的提示词。否则,嵌入式标签将被忽略",
|
||||
"Import Card Tags": "导入卡片标签",
|
||||
"Hide character definitions from the editor panel behind a spoiler button": "在编辑器面板中,将角色定义隐藏在一个剧透按钮后面。",
|
||||
"Spoiler Free Mode": "隐藏角色卡信息",
|
||||
"Miscellaneous": "杂项",
|
||||
@ -904,6 +907,8 @@
|
||||
"popup-button-no": "否",
|
||||
"popup-button-cancel": "取消",
|
||||
"popup-button-import": "导入",
|
||||
"popup-button-crop": "裁剪",
|
||||
"Close popup": "关闭弹出窗口",
|
||||
"Advanced Defininitions": "高级定义",
|
||||
"Prompt Overrides": "提示词覆盖",
|
||||
"(For Chat Completion and Instruct Mode)": "(用于聊天补全和指导模式)",
|
||||
@ -1016,9 +1021,6 @@
|
||||
"Prevent further recursion (this entry will not activate others)": "防止进一步递归(本条目将不会激活其他条目)",
|
||||
"Delay until recursion (this entry can only be activated on recursive checking)": "延迟到递归(本条目只能在递归检查时激活)",
|
||||
"What this keyword should mean to the AI, sent verbatim": "这个关键词对AI的含义,逐字发送",
|
||||
"Filter to Character(s)": "应用到角色",
|
||||
"Character Exclusion": "反选角色",
|
||||
"-- Characters not found --": "-- 未找到角色 --",
|
||||
"Inclusion Group": "包含组",
|
||||
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "包含组可确保每次仅激活组中的一项(如果触发了多项)。支持多个逗号分隔的组。文档:世界信息 - 包含组",
|
||||
"Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "优先考虑此条目:选中后,此条目在所有选择中优先考虑。如果优先考虑多个条目,则选择“顺序”最高的条目。",
|
||||
@ -1026,6 +1028,13 @@
|
||||
"Only one entry with the same label will be activated": "只有一个带有相同标签的条目将被激活",
|
||||
"A relative likelihood of entry activation within the group": "组内进入激活的相对可能性",
|
||||
"Group Weight": "组权重",
|
||||
"Sticky entries will stay active for N messages after being triggered.": "粘性条目在被触发后将保持活跃状态 N 条消息。",
|
||||
"Sticky": "粘性",
|
||||
"Entries with a cooldown can't be activated N messages after being triggered.": "具有冷却时间的条目在触发后 N 条消息内无法被激活。",
|
||||
"Cooldown": "冷却",
|
||||
"Filter to Character(s)": "应用到角色",
|
||||
"Character Exclusion": "反选角色",
|
||||
"-- Characters not found --": "-- 未找到角色 --",
|
||||
"Selective": "选择性",
|
||||
"Use Probability": "使用概率",
|
||||
"Add Memo": "添加备忘录",
|
||||
@ -1177,7 +1186,6 @@
|
||||
"Assets URL": "资产网址",
|
||||
"Characters": "人物",
|
||||
"Attach a File": "附加文件",
|
||||
"Open Data Bank": "开放数据库",
|
||||
"Enter a URL or the ID of a Fandom wiki page to scrape:": "输入要抓取的 Fandom wiki 页面的 URL 或 ID:",
|
||||
"Examples:": "例:",
|
||||
"Example:": "例:",
|
||||
@ -1185,7 +1193,8 @@
|
||||
"All articles will be concatenated into a single file.": "所有文章将被合并为一个文件。",
|
||||
"File per article": "每篇文章的文件数",
|
||||
"Each article will be saved as a separate file.": "不推荐。每篇文章将保存为单独的文件。",
|
||||
"Data Bank": "数据银行",
|
||||
"Open Data Bank": "打开数据库",
|
||||
"Data Bank": "数据库",
|
||||
"These files will be available for extensions that support attachments (e.g. Vector Storage).": "这些文件将可用于支持附件的扩展(例如 Vector Storage)。",
|
||||
"Supported file types: Plain Text, PDF, Markdown, HTML, EPUB.": "支持的文件类型:纯文本、PDF、Markdown、HTML、EPUB。",
|
||||
"Drag and drop files here to upload.": "将文件拖放到此处进行上传。",
|
||||
@ -1237,7 +1246,6 @@
|
||||
"Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)",
|
||||
"Classifier API": "分类器 API",
|
||||
"Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
|
||||
"Expression API": "本地\n附加\nLLM",
|
||||
"LLM": "大语言模型",
|
||||
"LLM Prompt": "大语言模型提示词",
|
||||
"Will be used if the API doesn't support JSON schemas or function calling.": "如果 API 不支持 JSON 模式或函数调用,则会使用它。",
|
||||
@ -1432,24 +1440,28 @@
|
||||
"Delete workflow": "删除工作流",
|
||||
"Enhance": "提高",
|
||||
"Refine": "优化",
|
||||
"Decrisper": "去伪器",
|
||||
"Sampling method": "采样方法",
|
||||
"Scheduler": "调度器",
|
||||
"Resolution": "分辨率",
|
||||
"Upscaler": "图像扩大器",
|
||||
"Sampling steps": "采样步数()",
|
||||
"Width": "宽度()",
|
||||
"Height": "高度()",
|
||||
"Resolution": "分辨率",
|
||||
"Sampling method": "采样方法",
|
||||
"Karras (not all samplers supported)": "Karras(并非所有采样器都受支持)",
|
||||
"Swap width and height": "交换宽度和高度",
|
||||
"Upscale by": "扩大倍数",
|
||||
"Denoising strength": "去噪强度",
|
||||
"Hires steps (2nd pass)": "高清修复步数(第二遍)",
|
||||
"CLIP Skip": "片段跳过",
|
||||
"Restore Faces": "面部修复",
|
||||
"Hires. Fix": "高清修复",
|
||||
"Karras": "Karras",
|
||||
"Not all samplers supported.": "并非所有采样器都受支持。",
|
||||
"SMEA versions of samplers are modified to perform better at high resolution.": "SMEA 版本的采样器经过修改,在高分辨率下性能更佳。",
|
||||
"SMEA": "中小企业协会",
|
||||
"DYN variants of SMEA samplers often lead to more varied output, but may fail at very high resolutions.": "SMEA 采样器的 DYN 变体通常会产生更加多样化的输出,但在非常高的分辨率下可能会失败。",
|
||||
"DYN": "动态",
|
||||
"Scheduler": "调度器",
|
||||
"Restore Faces": "面部修复",
|
||||
"Hires. Fix": "高清修复",
|
||||
"Upscaler": "图像扩大器",
|
||||
"Upscale by": "扩大倍数",
|
||||
"Denoising strength": "去噪强度",
|
||||
"Hires steps (2nd pass)": "高清修复步数(第二遍)",
|
||||
"Decrisper": "去伪器",
|
||||
"(-1 for random)": "(-1 为随机)",
|
||||
"Preset for prompt prefix and negative prompt": "提示词前缀和负面提示词的预设",
|
||||
"Style": "风格",
|
||||
"Save style": "保存风格",
|
||||
@ -1482,14 +1494,14 @@
|
||||
"ext_translate_mode_provider": "提供者",
|
||||
"ext_translate_target_lang": "目标语言",
|
||||
"ext_translate_clear": "清空设置",
|
||||
"Audio Playback Speed": "音频播放速度",
|
||||
"Vector Storage": "向量存储",
|
||||
"Vectorization Source": "向量化源",
|
||||
"Local (Transformers)": "本地(Transformers)",
|
||||
"Vectorization Model": "向量化模型",
|
||||
"Keep model in memory": "将模型保存在内存中",
|
||||
"Hint: Download models and set the URL in the API connection settings.": "提示:下载模型并在 API 连接设置中设置 URL。",
|
||||
"The server MUST be started with the --embedding flag to use this feature!": "服务器必须使用 --embedding 标志启动才能使用此功能!",
|
||||
"Hint: Set the URL in the API connection settings.": "提示:在 API 连接设置中设置 URL。",
|
||||
"The server MUST be started with the --embedding flag to use this feature!": "服务器必须使用 --embedding 标志启动才能使用此功能!",
|
||||
"Vectors Model Warning": "建议在聊天过程中更改模型时清除向量。否则会导致结果不理想。",
|
||||
"NomicAI API Key": "NomicAI API 密钥",
|
||||
"Query messages": "查询消息",
|
||||
@ -1540,6 +1552,19 @@
|
||||
"Current Password:": "当前密码:",
|
||||
"New Password:": "新密码:",
|
||||
"Confirm New Password:": "确认新密码:",
|
||||
"Import Tags For _begin": "为",
|
||||
"Import Tags For _end": "导入标签",
|
||||
"Click remove on any tag to remove it from this import.<br />Select one of the import options to finish importing the tags.": "单击任意标签上的“删除”可将其从本次导入中删除。\n选择其中一个导入选项以完成标签的导入。",
|
||||
"Existing Tags": "现有标签",
|
||||
"New Tags": "新标签",
|
||||
"Folder Tags": "文件夹标签",
|
||||
"The following tags will be auto-imported based on the currently selected folders": "根据当前选定的文件夹将自动导入以下标签",
|
||||
"Remember my choice": "记住我的选择",
|
||||
"Remember the chosen import option If anything besides 'Cancel' is selected, this dialog will not show up anymore. To change this, go to the settings and modify \"Tag Import Option\". If the \"Import\" option is chosen, the global setting will stay on \"Ask\".": "记住所选的导入选项\n如果选择了“取消”以外的任何选项,此对话框将不再显示。\n要更改此设置,请转到设置并修改“标签导入选项”。\n\n如果选择了“导入”选项,则全局设置将保留为“询问”。",
|
||||
"Import None": "不导入",
|
||||
"Import All": "全部导入",
|
||||
"Import Existing": "导入现有",
|
||||
"Import tags button": "导入",
|
||||
"Include Body Parameters": "包括主体参数",
|
||||
"custom_include_body_desc": "聊天完成请求主体中要包含的参数(YAML 对象)\n\n示例:\n- top_k:20\n- repetition_penalty:1.1",
|
||||
"Exclude Body Parameters": "排除主体参数",
|
||||
@ -1660,6 +1685,8 @@
|
||||
"help_macros_19": "当前选定的 API 的文本生成模型名称。",
|
||||
"Can be inaccurate!": "可能不准确!",
|
||||
"help_macros_20": "最新聊天消息的文本。",
|
||||
"help_macros_lastUser": "最后的用户聊天消息文本。",
|
||||
"help_macros_lastChar": "最后的角色聊天消息文本。",
|
||||
"help_macros_21": "最新聊天消息的索引号。对于斜线命令批处理很有用。",
|
||||
"help_macros_22": "上下文中包含的第一条消息的 ID。要求在当前会话中至少运行一次生成。",
|
||||
"help_macros_23": "最后一条聊天消息中当前滑动的 ID(以 1 为基数)。如果最后一条消息是用户或提示隐藏的,则为空字符串。",
|
||||
|
240
public/script.js
240
public/script.js
@ -226,9 +226,9 @@ import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay
|
||||
import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js';
|
||||
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
|
||||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.js';
|
||||
import { MacrosParser, evaluateMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
||||
import { ScraperManager } from './scripts/scrapers.js';
|
||||
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
|
||||
@ -454,6 +454,7 @@ export const event_types = {
|
||||
OPEN_CHARACTER_LIBRARY: 'open_character_library',
|
||||
LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
|
||||
LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
|
||||
ONLINE_STATUS_CHANGED: 'online_status_changed',
|
||||
};
|
||||
|
||||
export const eventSource = new EventEmitter();
|
||||
@ -513,9 +514,6 @@ let optionsPopper = Popper.createPopper(document.getElementById('options_button'
|
||||
let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), {
|
||||
placement: 'left',
|
||||
});
|
||||
let rawPromptPopper = Popper.createPopper(document.getElementById('dialogue_popup'), document.getElementById('rawPromptPopup'), {
|
||||
placement: 'right',
|
||||
});
|
||||
|
||||
// Saved here for performance reasons
|
||||
const messageTemplate = $('#message_template .mes');
|
||||
@ -525,7 +523,7 @@ let dialogueResolve = null;
|
||||
let dialogueCloseStop = false;
|
||||
export let chat_metadata = {};
|
||||
export let streamingProcessor = null;
|
||||
export let crop_data = undefined;
|
||||
let crop_data = undefined;
|
||||
let is_delete_mode = false;
|
||||
let fav_ch_checked = false;
|
||||
let scrollLock = false;
|
||||
@ -1053,10 +1051,10 @@ export async function clearItemizedPrompts() {
|
||||
async function getStatusHorde() {
|
||||
try {
|
||||
const hordeStatus = await checkHordeStatus();
|
||||
online_status = hordeStatus ? 'Connected' : 'no_connection';
|
||||
setOnlineStatus(hordeStatus ? 'Connected' : 'no_connection');
|
||||
}
|
||||
catch {
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
}
|
||||
|
||||
return resultCheckStatus();
|
||||
@ -1067,7 +1065,7 @@ async function getStatusKobold() {
|
||||
|
||||
if (!endpoint) {
|
||||
console.warn('No endpoint for status check');
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
@ -1084,7 +1082,7 @@ async function getStatusKobold() {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
online_status = data?.model ?? 'no_connection';
|
||||
setOnlineStatus(data?.model ?? 'no_connection');
|
||||
|
||||
if (!data.koboldUnitedVersion) {
|
||||
throw new Error('Missing mandatory Kobold version in data:', data);
|
||||
@ -1102,7 +1100,7 @@ async function getStatusKobold() {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error getting status', err);
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
}
|
||||
|
||||
return resultCheckStatus();
|
||||
@ -1115,12 +1113,12 @@ async function getStatusTextgen() {
|
||||
|
||||
if (!endpoint) {
|
||||
console.warn('No endpoint for status check');
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
if (textgen_settings.type == OOBA && textgen_settings.bypass_status_check) {
|
||||
online_status = 'Status check bypassed';
|
||||
setOnlineStatus('Status check bypassed');
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
@ -1140,34 +1138,34 @@ async function getStatusTextgen() {
|
||||
|
||||
if (textgen_settings.type === MANCER) {
|
||||
loadMancerModels(data?.data);
|
||||
online_status = textgen_settings.mancer_model;
|
||||
setOnlineStatus(textgen_settings.mancer_model);
|
||||
} else if (textgen_settings.type === TOGETHERAI) {
|
||||
loadTogetherAIModels(data?.data);
|
||||
online_status = textgen_settings.togetherai_model;
|
||||
setOnlineStatus(textgen_settings.togetherai_model);
|
||||
} else if (textgen_settings.type === OLLAMA) {
|
||||
loadOllamaModels(data?.data);
|
||||
online_status = textgen_settings.ollama_model || 'Connected';
|
||||
setOnlineStatus(textgen_settings.ollama_model || 'Connected');
|
||||
} else if (textgen_settings.type === INFERMATICAI) {
|
||||
loadInfermaticAIModels(data?.data);
|
||||
online_status = textgen_settings.infermaticai_model;
|
||||
setOnlineStatus(textgen_settings.infermaticai_model);
|
||||
} else if (textgen_settings.type === DREAMGEN) {
|
||||
loadDreamGenModels(data?.data);
|
||||
online_status = textgen_settings.dreamgen_model;
|
||||
setOnlineStatus(textgen_settings.dreamgen_model);
|
||||
} else if (textgen_settings.type === OPENROUTER) {
|
||||
loadOpenRouterModels(data?.data);
|
||||
online_status = textgen_settings.openrouter_model;
|
||||
setOnlineStatus(textgen_settings.openrouter_model);
|
||||
} else if (textgen_settings.type === VLLM) {
|
||||
loadVllmModels(data?.data);
|
||||
online_status = textgen_settings.vllm_model;
|
||||
setOnlineStatus(textgen_settings.vllm_model);
|
||||
} else if (textgen_settings.type === APHRODITE) {
|
||||
loadAphroditeModels(data?.data);
|
||||
online_status = textgen_settings.aphrodite_model;
|
||||
setOnlineStatus(textgen_settings.aphrodite_model);
|
||||
} else {
|
||||
online_status = data?.result;
|
||||
setOnlineStatus(data?.result);
|
||||
}
|
||||
|
||||
if (!online_status) {
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
}
|
||||
|
||||
// Determine instruct mode preset
|
||||
@ -1179,7 +1177,7 @@ async function getStatusTextgen() {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error getting status', err);
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
}
|
||||
|
||||
return resultCheckStatus();
|
||||
@ -1193,9 +1191,9 @@ async function getStatusNovel() {
|
||||
throw new Error('Could not load subscription data');
|
||||
}
|
||||
|
||||
online_status = getNovelTier();
|
||||
setOnlineStatus(getNovelTier());
|
||||
} catch {
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
}
|
||||
|
||||
resultCheckStatus();
|
||||
@ -4728,17 +4726,14 @@ function addChatsSeparator(mesSendString) {
|
||||
}
|
||||
}
|
||||
|
||||
async function DupeChar() {
|
||||
async function duplicateCharacter() {
|
||||
if (!this_chid) {
|
||||
toastr.warning('You must first select a character to duplicate!');
|
||||
return '';
|
||||
}
|
||||
|
||||
const confirmMessage = `
|
||||
<h3>Are you sure you want to duplicate this character?</h3>
|
||||
<span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span><br><br>`;
|
||||
|
||||
const confirm = await callPopup(confirmMessage, 'confirm');
|
||||
const confirmMessage = $(await renderTemplateAsync('duplicateConfirm'));
|
||||
const confirm = await callGenericPopup(confirmMessage, POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (!confirm) {
|
||||
console.log('User cancelled duplication');
|
||||
@ -4755,7 +4750,7 @@ async function DupeChar() {
|
||||
toastr.success('Character Duplicated');
|
||||
const data = await response.json();
|
||||
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
|
||||
getCharacters();
|
||||
await getCharacters();
|
||||
}
|
||||
|
||||
return '';
|
||||
@ -4893,14 +4888,44 @@ async function promptItemize(itemizedPrompts, requestedMesId) {
|
||||
|
||||
const params = await itemizedParams(itemizedPrompts, thisPromptSet);
|
||||
|
||||
if (params.this_main_api == 'openai') {
|
||||
const template = await renderTemplateAsync('itemizationChat', params);
|
||||
callPopup(template, 'text');
|
||||
const template = params.this_main_api == 'openai'
|
||||
? await renderTemplateAsync('itemizationChat', params)
|
||||
: await renderTemplateAsync('itemizationText', params);
|
||||
|
||||
} else {
|
||||
const template = await renderTemplateAsync('itemizationText', params);
|
||||
callPopup(template, 'text');
|
||||
}
|
||||
const popup = new Popup(template, POPUP_TYPE.TEXT);
|
||||
|
||||
popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('click', function () {
|
||||
let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
|
||||
let rawPromptValues = rawPrompt;
|
||||
|
||||
if (Array.isArray(rawPrompt)) {
|
||||
rawPromptValues = rawPrompt.map(x => x.content).join('\n');
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(rawPromptValues);
|
||||
toastr.info('Copied!');
|
||||
});
|
||||
|
||||
popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () {
|
||||
//console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
|
||||
console.log(PromptArrayItemForRawPromptDisplay);
|
||||
console.log(itemizedPrompts);
|
||||
console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
|
||||
|
||||
let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
|
||||
let rawPromptValues = rawPrompt;
|
||||
|
||||
if (Array.isArray(rawPrompt)) {
|
||||
rawPromptValues = rawPrompt.map(x => x.content).join('\n');
|
||||
}
|
||||
|
||||
//let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
|
||||
const rawPromptWrapper = document.getElementById('rawPromptWrapper');
|
||||
rawPromptWrapper.innerText = rawPromptValues;
|
||||
$('#rawPromptPopup').slideToggle();
|
||||
});
|
||||
|
||||
await popup.show();
|
||||
}
|
||||
|
||||
function setInContextMessages(lastmsg, type) {
|
||||
@ -5502,9 +5527,17 @@ export function setCharacterName(value) {
|
||||
name2 = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API connection status of the application
|
||||
* @param {string|'no_connection'} value Connection status value
|
||||
*/
|
||||
export function setOnlineStatus(value) {
|
||||
const previousStatus = online_status;
|
||||
online_status = value;
|
||||
displayOnlineStatus();
|
||||
if (previousStatus !== online_status) {
|
||||
eventSource.emitAndWait(event_types.ONLINE_STATUS_CHANGED, online_status);
|
||||
}
|
||||
}
|
||||
|
||||
export function setEditedMessageId(value) {
|
||||
@ -5754,17 +5787,20 @@ async function read_avatar_load(input) {
|
||||
create_save.avatar = input.files;
|
||||
}
|
||||
|
||||
crop_data = undefined;
|
||||
const file = input.files[0];
|
||||
const fileData = await getBase64Async(file);
|
||||
|
||||
if (!power_user.never_resize_avatars) {
|
||||
$('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
|
||||
const croppedImage = await callPopup(getCropPopup(fileData), 'avatarToCrop');
|
||||
const dlg = new Popup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropImage: fileData });
|
||||
const croppedImage = await dlg.show();
|
||||
|
||||
if (!croppedImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#avatar_load_preview').attr('src', croppedImage);
|
||||
crop_data = dlg.cropData;
|
||||
$('#avatar_load_preview').attr('src', String(croppedImage));
|
||||
} else {
|
||||
$('#avatar_load_preview').attr('src', fileData);
|
||||
}
|
||||
@ -5807,13 +5843,6 @@ async function read_avatar_load(input) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getCropPopup(src) {
|
||||
return `<h3>Set the crop position of the avatar image and click Accept to confirm.</h3>
|
||||
<div id='avatarCropWrap'>
|
||||
<img id='avatarToCrop' src='${src}'>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function getThumbnailUrl(type, file) {
|
||||
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
|
||||
}
|
||||
@ -6076,7 +6105,7 @@ export function changeMainAPI() {
|
||||
}
|
||||
|
||||
main_api = selectedVal;
|
||||
online_status = 'no_connection';
|
||||
setOnlineStatus('no_connection');
|
||||
|
||||
if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
$('#api_button_openai').trigger('click');
|
||||
@ -7122,9 +7151,7 @@ function onScenarioOverrideRemoveClick() {
|
||||
*/
|
||||
export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
|
||||
function getOkButtonText() {
|
||||
if (['avatarToCrop'].includes(popup_type)) {
|
||||
return okButton ?? 'Accept';
|
||||
} else if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {
|
||||
if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {
|
||||
$dialoguePopupCancel.css('display', 'none');
|
||||
return okButton ?? 'Ok';
|
||||
} else if (['delete_extension'].includes(popup_type)) {
|
||||
@ -7165,22 +7192,6 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w
|
||||
$dialoguePopupInput.trigger('focus');
|
||||
}
|
||||
|
||||
if (popup_type == 'avatarToCrop') {
|
||||
// unset existing data
|
||||
crop_data = undefined;
|
||||
|
||||
$('#avatarToCrop').cropper({
|
||||
aspectRatio: cropAspect ?? 2 / 3,
|
||||
autoCropArea: 1,
|
||||
viewMode: 2,
|
||||
rotatable: false,
|
||||
crop: function (event) {
|
||||
crop_data = event.detail;
|
||||
crop_data.want_resize = !power_user.never_resize_avatars;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$shadowPopup.transition({
|
||||
opacity: 1,
|
||||
duration: animation_duration,
|
||||
@ -7788,6 +7799,7 @@ window['SillyTavern'].getContext = function () {
|
||||
* @deprecated Handlebars for extensions are no longer supported.
|
||||
*/
|
||||
registerHelper: () => { },
|
||||
registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
|
||||
registedDebugFunction: registerDebugFunction,
|
||||
/**
|
||||
* @deprecated Use renderExtensionTemplateAsync instead.
|
||||
@ -8829,7 +8841,7 @@ jQuery(async function () {
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'dupe',
|
||||
callback: DupeChar,
|
||||
callback: duplicateCharacter,
|
||||
helpString: 'Duplicates the currently selected character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
@ -9179,12 +9191,6 @@ jQuery(async function () {
|
||||
$('#dialogue_popup').removeClass('wide_dialogue_popup');
|
||||
}, animation_duration);
|
||||
|
||||
// $("#shadow_popup").css("opacity:", 0.0);
|
||||
|
||||
if (popup_type == 'avatarToCrop') {
|
||||
dialogueResolve($('#avatarToCrop').data('cropper').getCroppedCanvas().toDataURL('image/jpeg'));
|
||||
}
|
||||
|
||||
if (popup_type == 'del_chat') {
|
||||
//close past chat popup
|
||||
$('#select_chat_cross').trigger('click');
|
||||
@ -9214,13 +9220,6 @@ jQuery(async function () {
|
||||
if (popup_type == 'alternate_greeting' && menu_type !== 'create') {
|
||||
createOrEditCharacter();
|
||||
}
|
||||
if (popup_type === 'del_group') {
|
||||
const groupId = $('#dialogue_popup').data('group_id');
|
||||
|
||||
if (groupId) {
|
||||
deleteGroup(groupId);
|
||||
}
|
||||
}
|
||||
//Make a new chat for selected character
|
||||
if (
|
||||
popup_type == 'new_chat' &&
|
||||
@ -9254,9 +9253,6 @@ jQuery(async function () {
|
||||
}
|
||||
}
|
||||
|
||||
rawPromptPopper.update();
|
||||
$('#rawPromptPopup').hide();
|
||||
|
||||
if (dialogueResolve) {
|
||||
if (popup_type == 'input') {
|
||||
dialogueResolve($('#dialogue_popup_input').val());
|
||||
@ -9847,45 +9843,14 @@ jQuery(async function () {
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('pointerup', '.mes_prompt', function () {
|
||||
$(document).on('pointerup', '.mes_prompt', async function () {
|
||||
let mesIdForItemization = $(this).closest('.mes').attr('mesId');
|
||||
console.log(`looking for mesID: ${mesIdForItemization}`);
|
||||
if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
|
||||
promptItemize(itemizedPrompts, mesIdForItemization);
|
||||
await promptItemize(itemizedPrompts, mesIdForItemization);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('pointerup', '#copyPromptToClipboard', function () {
|
||||
let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
|
||||
let rawPromptValues = rawPrompt;
|
||||
|
||||
if (Array.isArray(rawPrompt)) {
|
||||
rawPromptValues = rawPrompt.map(x => x.content).join('\n');
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(rawPromptValues);
|
||||
toastr.info('Copied!', '', { timeOut: 2000 });
|
||||
});
|
||||
|
||||
$(document).on('pointerup', '#showRawPrompt', function () {
|
||||
//console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
|
||||
console.log(PromptArrayItemForRawPromptDisplay);
|
||||
console.log(itemizedPrompts);
|
||||
console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
|
||||
|
||||
let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
|
||||
let rawPromptValues = rawPrompt;
|
||||
|
||||
if (Array.isArray(rawPrompt)) {
|
||||
rawPromptValues = rawPrompt.map(x => x.content).join('\n');
|
||||
}
|
||||
|
||||
//let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
|
||||
$('#rawPromptWrapper').text(rawPromptValues);
|
||||
rawPromptPopper.update();
|
||||
$('#rawPromptPopup').toggle();
|
||||
});
|
||||
|
||||
//********************
|
||||
//***Message Editor***
|
||||
$(document).on('click', '.mes_edit', async function () {
|
||||
@ -10090,7 +10055,7 @@ jQuery(async function () {
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_edit_copy', async function () {
|
||||
const confirmation = await callPopup('Create a copy of this message?', 'confirm');
|
||||
const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM);
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
@ -10116,24 +10081,33 @@ jQuery(async function () {
|
||||
|
||||
$(document).on('click', '.mes_edit_delete', async function (event, customData) {
|
||||
const fromSlashCommand = customData?.fromSlashCommand || false;
|
||||
const swipeExists = (!Array.isArray(chat[this_edit_mes_id].swipes) || chat[this_edit_mes_id].swipes.length <= 1 || chat[this_edit_mes_id].is_user || parseInt(this_edit_mes_id) !== chat.length - 1);
|
||||
const canDeleteSwipe = (Array.isArray(chat[this_edit_mes_id].swipes) && chat[this_edit_mes_id].swipes.length > 1 && !chat[this_edit_mes_id].is_user && parseInt(this_edit_mes_id) === chat.length - 1);
|
||||
|
||||
let deleteOnlySwipe = false;
|
||||
if (power_user.confirm_message_delete && fromSlashCommand !== true) {
|
||||
const confirmation = swipeExists ? await callPopup('Are you sure you want to delete this message?', 'confirm')
|
||||
: await callPopup('<h3>Delete this...</h3> <select id=\'del_type\'><option value=\'swipe\'>Swipe</option><option value=\'message\'>Message</option></select>', 'confirm');
|
||||
if (!confirmation) {
|
||||
const result = await callGenericPopup('Are you sure you want to delete this message?', POPUP_TYPE.CONFIRM, null, {
|
||||
okButton: canDeleteSwipe ? 'Delete Swipe' : 'Delete Message',
|
||||
cancelButton: 'Cancel',
|
||||
customButtons: canDeleteSwipe ? ['Delete Message'] : null,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
deleteOnlySwipe = canDeleteSwipe && result === 1; // Default button, not the custom one
|
||||
}
|
||||
|
||||
const mes = $(this).closest('.mes');
|
||||
|
||||
if (!mes) {
|
||||
const messageElement = $(this).closest('.mes');
|
||||
if (!messageElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#del_type').val() === 'swipe') {
|
||||
const swipe_id = chat[this_edit_mes_id]['swipe_id'];
|
||||
chat[this_edit_mes_id]['swipes'].splice(swipe_id, 1);
|
||||
if (deleteOnlySwipe) {
|
||||
const message = chat[this_edit_mes_id];
|
||||
const swipe_id = message.swipe_id;
|
||||
message.swipes.splice(swipe_id, 1);
|
||||
if (Array.isArray(message.swipe_info) && message.swipe_info.length) {
|
||||
message.swipe_info.splice(swipe_id, 1);
|
||||
}
|
||||
if (swipe_id > 0) {
|
||||
$('.swipe_left:last').click();
|
||||
} else {
|
||||
@ -10141,7 +10115,7 @@ jQuery(async function () {
|
||||
}
|
||||
} else {
|
||||
chat.splice(this_edit_mes_id, 1);
|
||||
mes.remove();
|
||||
messageElement.remove();
|
||||
}
|
||||
|
||||
let startFromZero = Number(this_edit_mes_id) === 0;
|
||||
@ -10272,7 +10246,7 @@ jQuery(async function () {
|
||||
});
|
||||
|
||||
$('#dupe_button').click(async function () {
|
||||
await DupeChar();
|
||||
await duplicateCharacter();
|
||||
});
|
||||
|
||||
$(document).on('click', '.select_chat_block, .bookmark_link, .mes_bookmark', async function () {
|
||||
|
@ -16,7 +16,6 @@ import {
|
||||
eventSource,
|
||||
menu_type,
|
||||
substituteParams,
|
||||
callPopup,
|
||||
sendTextareaMessage,
|
||||
} from '../script.js';
|
||||
|
||||
@ -1004,20 +1003,21 @@ export function initRossMods() {
|
||||
if (skipConfirm) {
|
||||
doRegenerate();
|
||||
} else {
|
||||
const popupText = `
|
||||
<div class="marginBot10">Are you sure you want to regenerate the latest message?</div>
|
||||
<label class="checkbox_label justifyCenter" for="regenerateWithCtrlEnter">
|
||||
<input type="checkbox" id="regenerateWithCtrlEnter">
|
||||
Don't ask again
|
||||
</label>`;
|
||||
callPopup(popupText, 'confirm').then(result => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const regenerateWithCtrlEnter = $('#regenerateWithCtrlEnter').prop('checked');
|
||||
SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);
|
||||
doRegenerate();
|
||||
});
|
||||
Popup.show.confirm('Regenerate Message', `
|
||||
<span>Are you sure you want to regenerate the latest message?</span>
|
||||
<label class="checkbox_label justifyCenter marginTop10" for="regenerateWithCtrlEnter">
|
||||
<input type="checkbox" id="regenerateWithCtrlEnter">
|
||||
Don't ask again
|
||||
</label>`, {
|
||||
onClose: (popup) => {
|
||||
if (!popup.result) {
|
||||
return;
|
||||
}
|
||||
const regenerateWithCtrlEnter = $('#regenerateWithCtrlEnter').prop('checked');
|
||||
SaveLocal(skipConfirmKey, regenerateWithCtrlEnter);
|
||||
doRegenerate();
|
||||
},
|
||||
})
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
|
@ -4,7 +4,6 @@ import css from '../lib/css-parser.mjs';
|
||||
import {
|
||||
addCopyToCodeBlocks,
|
||||
appendMediaToMessage,
|
||||
callPopup,
|
||||
characters,
|
||||
chat,
|
||||
eventSource,
|
||||
@ -38,6 +37,7 @@ import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { ScraperManager } from './scrapers.js';
|
||||
import { DragAndDropHandler } from './dragdrop.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} FileAttachment
|
||||
@ -524,7 +524,7 @@ async function openExternalMediaOverridesDialog() {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $('#forbid_media_override_template > .forbid_media_override').clone();
|
||||
const template = $(await renderTemplateAsync('forbidMedia'));
|
||||
template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_media);
|
||||
template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_media);
|
||||
|
||||
@ -538,7 +538,7 @@ async function openExternalMediaOverridesDialog() {
|
||||
template.find('#forbid_media_override_global').prop('checked', true);
|
||||
}
|
||||
|
||||
callPopup(template, 'text', '', { wide: false, large: false });
|
||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: false, large: false });
|
||||
}
|
||||
|
||||
export function getCurrentEntityId() {
|
||||
@ -1119,7 +1119,7 @@ async function openAttachmentManager() {
|
||||
const cleanupFn = await renderButtons();
|
||||
await verifyAttachments();
|
||||
await renderAttachments();
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close', allowVerticalScrolling: true });
|
||||
|
||||
cleanupFn();
|
||||
dragDropHandler.destroy();
|
||||
@ -1465,7 +1465,7 @@ jQuery(function () {
|
||||
});
|
||||
}
|
||||
|
||||
callPopup(wrapper, 'text', '', { wide: true, large: true });
|
||||
callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
});
|
||||
|
||||
$(document).on('click', 'body.documentstyle .mes .mes_text', function () {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
|
||||
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
|
||||
import { hideLoader, showLoader } from './loader.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './templates.js';
|
||||
import { isSubsetOf, setValueByPath } from './utils.js';
|
||||
export {
|
||||
@ -503,7 +504,7 @@ function addExtensionScript(name, manifest) {
|
||||
* @param {boolean} isDisabled - Whether the extension is disabled or not.
|
||||
* @param {boolean} isExternal - Whether the extension is external or not.
|
||||
* @param {string} checkboxClass - The class for the checkbox HTML element.
|
||||
* @return {string} - The HTML string that represents the extension.
|
||||
* @return {Promise<string>} - The HTML string that represents the extension.
|
||||
*/
|
||||
async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) {
|
||||
const displayName = manifest.display_name;
|
||||
@ -555,8 +556,10 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt
|
||||
} else if (!isDisabled) { // Neither active nor disabled
|
||||
const requirements = new Set(manifest.requires);
|
||||
modules.forEach(x => requirements.delete(x));
|
||||
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
|
||||
extensionHtml += `<p>Missing modules: <span class="failure">${requirementsString}</span></p>`;
|
||||
if (requirements.size > 0) {
|
||||
const requirementsString = DOMPurify.sanitize([...requirements].join(', '));
|
||||
extensionHtml += `<p>Missing modules: <span class="failure">${requirementsString}</span></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
return extensionHtml;
|
||||
@ -631,7 +634,18 @@ async function showExtensionsDetails() {
|
||||
${htmlDefault}
|
||||
${htmlExternal}
|
||||
`;
|
||||
popupPromise = callPopup(`<div class="extensions_info">${html}</div>`, 'text', '', { okButton: 'Close', wide: true, large: true });
|
||||
/** @type {import('./popup.js').CustomPopupButton} */
|
||||
const updateAllButton = {
|
||||
text: 'Update all',
|
||||
appendAtEnd: true,
|
||||
action: async () => {
|
||||
requiresReload = true;
|
||||
await autoUpdateExtensions(true);
|
||||
popup.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
},
|
||||
};
|
||||
const popup = new Popup(`<div class="extensions_info">${html}</div>`, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });
|
||||
popupPromise = popup.show();
|
||||
} catch (error) {
|
||||
toastr.error('Error loading extensions. See browser console for details.');
|
||||
console.error(error);
|
||||
@ -700,8 +714,8 @@ async function updateExtension(extensionName, quiet) {
|
||||
async function onDeleteClick() {
|
||||
const extensionName = $(this).data('name');
|
||||
// use callPopup to create a popup for the user to confirm before delete
|
||||
const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension');
|
||||
if (confirmation) {
|
||||
const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});
|
||||
if (confirmation === POPUP_RESULT.AFFIRMATIVE) {
|
||||
await deleteExtension(extensionName);
|
||||
}
|
||||
}
|
||||
@ -797,7 +811,7 @@ async function loadExtensionSettings(settings, versionChanged) {
|
||||
manifests = await getManifests(extensionNames);
|
||||
|
||||
if (versionChanged) {
|
||||
await autoUpdateExtensions();
|
||||
await autoUpdateExtensions(false);
|
||||
}
|
||||
|
||||
await activateExtensions();
|
||||
@ -860,7 +874,12 @@ async function checkForExtensionUpdates(force) {
|
||||
}
|
||||
}
|
||||
|
||||
async function autoUpdateExtensions() {
|
||||
/**
|
||||
* Updates all 3rd-party extensions that have auto-update enabled.
|
||||
* @param {boolean} forceAll Force update all even if not auto-updating
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function autoUpdateExtensions(forceAll) {
|
||||
if (!Object.values(manifests).some(x => x.auto_update)) {
|
||||
return;
|
||||
}
|
||||
@ -868,7 +887,7 @@ async function autoUpdateExtensions() {
|
||||
const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 });
|
||||
const promises = [];
|
||||
for (const [id, manifest] of Object.entries(manifests)) {
|
||||
if (manifest.auto_update && id.startsWith('third-party')) {
|
||||
if ((forceAll || manifest.auto_update) && id.startsWith('third-party')) {
|
||||
console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`);
|
||||
promises.push(updateExtension(id.replace('third-party', ''), true));
|
||||
}
|
||||
@ -988,14 +1007,14 @@ jQuery(async function () {
|
||||
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
|
||||
<br>
|
||||
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`;
|
||||
const input = await callPopup(html, 'input');
|
||||
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '');
|
||||
|
||||
if (!input) {
|
||||
console.debug('Extension install cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = input.trim();
|
||||
const url = String(input).trim();
|
||||
await installExtension(url);
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { callPopup, main_api } from '../../../script.js';
|
||||
import { main_api } from '../../../script.js';
|
||||
import { getContext } from '../../extensions.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
||||
import { resetScrollHeight, debounce } from '../../utils.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
|
||||
function rgb2hex(rgb) {
|
||||
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
||||
@ -63,8 +64,7 @@ async function doTokenCounter() {
|
||||
}, debounce_timeout.relaxed);
|
||||
dialog.find('#token_counter_textarea').on('input', () => countDebounced());
|
||||
|
||||
$('#dialogue_popup').addClass('wide_dialogue_popup');
|
||||
callPopup(dialog, 'text', '', { wide: true, large: true });
|
||||
callGenericPopup(dialog, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,6 @@
|
||||
export { translate };
|
||||
|
||||
import {
|
||||
callPopup,
|
||||
eventSource,
|
||||
event_types,
|
||||
getRequestHeaders,
|
||||
@ -11,6 +10,7 @@ import {
|
||||
updateMessageBlock,
|
||||
} from '../../../script.js';
|
||||
import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { findSecret, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
@ -510,7 +510,7 @@ async function onTranslateChatClick() {
|
||||
|
||||
async function onTranslationsClearClick() {
|
||||
const popupHtml = await renderExtensionTemplateAsync('translate', 'deleteConfirmation');
|
||||
const confirm = await callPopup(popupHtml, 'confirm');
|
||||
const confirm = await callGenericPopup(popupHtml, POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
@ -598,7 +598,7 @@ jQuery(async () => {
|
||||
$(document).on('click', '.mes_translate', onMessageTranslateClick);
|
||||
$('#translate_key_button').on('click', async () => {
|
||||
const optionText = $('#translation_provider option:selected').text();
|
||||
const key = await callPopup(`<h3>${optionText} API Key</h3>`, 'input');
|
||||
const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT);
|
||||
|
||||
if (key == false) {
|
||||
return;
|
||||
@ -621,7 +621,7 @@ jQuery(async () => {
|
||||
const secretKey = extension_settings.translate.provider + '_url';
|
||||
const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
|
||||
|
||||
const url = await callPopup(popupText, 'input', savedUrl);
|
||||
const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl);
|
||||
|
||||
if (url == false || url == '') {
|
||||
return;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { callPopup, cancelTtsPlay, eventSource, event_types, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { cancelTtsPlay, eventSource, event_types, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
|
||||
import { EdgeTtsProvider } from './edge.js';
|
||||
@ -21,6 +21,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
@ -310,7 +311,7 @@ async function onTtsVoicesClick() {
|
||||
popupText = 'Could not load voices list. Check your API key.';
|
||||
}
|
||||
|
||||
callPopup(popupText, 'text');
|
||||
callGenericPopup(popupText, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true });
|
||||
}
|
||||
|
||||
function updateUiAudioPlayState() {
|
||||
@ -915,7 +916,7 @@ function getCharacters(unrestricted) {
|
||||
|
||||
function sanitizeId(input) {
|
||||
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
|
||||
let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
|
||||
let sanitized = encodeURIComponent(input).replace(/[^a-zA-Z0-9-_]/g, '');
|
||||
|
||||
// Ensure first character is always a letter
|
||||
if (!/^[a-zA-Z]/.test(sanitized)) {
|
||||
|
@ -276,7 +276,8 @@ class SBVits2TtsProvider {
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
|
||||
const [model_id, speaker_id, style] = voiceId.split('-');
|
||||
const [model_id, speaker_id, ...rest] = voiceId.split('-');
|
||||
const style = rest.join('-');
|
||||
const params = new URLSearchParams();
|
||||
// restore for auto_split
|
||||
inputText = inputText.replaceAll('<br>', '\n');
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
characters,
|
||||
default_avatar,
|
||||
addOneMessage,
|
||||
callPopup,
|
||||
clearChat,
|
||||
Generate,
|
||||
select_rm_info,
|
||||
@ -62,7 +61,6 @@ import {
|
||||
event_types,
|
||||
getCurrentChatId,
|
||||
setScenarioOverride,
|
||||
getCropPopup,
|
||||
system_avatar,
|
||||
isChatSaving,
|
||||
setExternalAbortController,
|
||||
@ -76,6 +74,7 @@ import {
|
||||
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
import { isExternalMediaAllowed } from './chats.js';
|
||||
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
|
||||
export {
|
||||
selected_group,
|
||||
@ -1300,14 +1299,20 @@ function isGroupMemberDisabled(avatarId) {
|
||||
return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId));
|
||||
}
|
||||
|
||||
function onDeleteGroupClick() {
|
||||
async function onDeleteGroupClick() {
|
||||
if (!openGroupId) {
|
||||
toastr.warning('Currently no group selected.');
|
||||
return;
|
||||
}
|
||||
if (is_group_generating) {
|
||||
toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#dialogue_popup').data('group_id', openGroupId);
|
||||
callPopup('<h3>Delete the group?</h3><p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>', 'del_group');
|
||||
const confirm = await Popup.show.confirm('Delete the group?', '<p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>');
|
||||
if (confirm) {
|
||||
deleteGroup(openGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onFavoriteGroupClick() {
|
||||
@ -1450,13 +1455,13 @@ async function uploadGroupAvatar(event) {
|
||||
|
||||
$('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
|
||||
|
||||
const croppedImage = await callPopup(getCropPopup(result), 'avatarToCrop');
|
||||
const croppedImage = await callGenericPopup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropImage: result });
|
||||
|
||||
if (!croppedImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
let thumbnail = await createThumbnail(croppedImage, 200, 300);
|
||||
let thumbnail = await createThumbnail(String(croppedImage), 200, 300);
|
||||
//remove data:image/whatever;base64
|
||||
thumbnail = thumbnail.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
let _thisGroup = groups.find((x) => x.id == openGroupId);
|
||||
@ -1476,8 +1481,7 @@ async function uploadGroupAvatar(event) {
|
||||
}
|
||||
|
||||
async function restoreGroupAvatar() {
|
||||
const confirm = await callPopup('<h3>Are you sure you want to restore the group avatar?</h3> Your custom image will be deleted, and a collage will be used instead.', 'confirm');
|
||||
|
||||
const confirm = await Popup.show.confirm('Are you sure you want to restore the group avatar?', 'Your custom image will be deleted, and a collage will be used instead.');
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';
|
||||
import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js';
|
||||
import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js';
|
||||
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
||||
import { replaceInstructMacros } from './instruct-mode.js';
|
||||
import { replaceVariableMacros } from './variables.js';
|
||||
@ -13,6 +13,108 @@ Handlebars.registerHelper('helperMissing', function () {
|
||||
return substituteParams(`{{${macroName}}}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object<string, *>} EnvObject
|
||||
* @typedef {(nonce: string) => string} MacroFunction
|
||||
*/
|
||||
|
||||
export class MacrosParser {
|
||||
/**
|
||||
* A map of registered macros.
|
||||
* @type {Map<string, string|MacroFunction>}
|
||||
*/
|
||||
static #macros = new Map();
|
||||
|
||||
/**
|
||||
* Registers a global macro that can be used anywhere where substitution is allowed.
|
||||
* @param {string} key Macro name (key)
|
||||
* @param {string|MacroFunction} value A string or a function that returns a string
|
||||
*/
|
||||
static registerMacro(key, value) {
|
||||
if (typeof key !== 'string') {
|
||||
throw new Error('Macro key must be a string');
|
||||
}
|
||||
|
||||
// Allowing surrounding whitespace would just create more confusion...
|
||||
key = key.trim();
|
||||
|
||||
if (!key) {
|
||||
throw new Error('Macro key must not be empty or whitespace only');
|
||||
}
|
||||
|
||||
if (key.startsWith('{{') || key.endsWith('}}')) {
|
||||
throw new Error('Macro key must not include the surrounding braces');
|
||||
}
|
||||
|
||||
if (typeof value !== 'string' && typeof value !== 'function') {
|
||||
console.warn(`Macro value for "${key}" will be converted to a string`);
|
||||
value = this.sanitizeMacroValue(value);
|
||||
}
|
||||
|
||||
if (this.#macros.has(key)) {
|
||||
console.warn(`Macro ${key} is already registered`);
|
||||
}
|
||||
|
||||
this.#macros.set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the env object with macro values from the current context.
|
||||
* @param {EnvObject} env Env object for the current evaluation context
|
||||
* @returns {void}
|
||||
*/
|
||||
static populateEnv(env) {
|
||||
if (!env || typeof env !== 'object') {
|
||||
console.warn('Env object is not provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// No macros are registered
|
||||
if (this.#macros.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of this.#macros) {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a type-check on the macro value and returns a sanitized version of it.
|
||||
* @param {any} value Value returned by a macro
|
||||
* @returns {string} Sanitized value
|
||||
*/
|
||||
static sanitizeMacroValue(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (value instanceof Promise) {
|
||||
console.warn('Promises are not supported as macro values');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'function') {
|
||||
console.warn('Functions are not supported as macro values');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a hashed id of the current chat from the metadata.
|
||||
* If no metadata exists, creates a new hash and saves it.
|
||||
@ -284,7 +386,7 @@ function timeDiffReplace(input) {
|
||||
/**
|
||||
* Substitutes {{macro}} parameters in a string.
|
||||
* @param {string} content - The string to substitute parameters in.
|
||||
* @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param
|
||||
* @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param
|
||||
* values are functions, those functions will be called and their return values are used.
|
||||
* @returns {string} The string with substituted parameters.
|
||||
*/
|
||||
@ -315,12 +417,19 @@ export function evaluateMacros(content, env) {
|
||||
content = content.replace(/{{noop}}/gi, '');
|
||||
content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val()));
|
||||
|
||||
// Add all registered macros to the env object
|
||||
const nonce = uuidv4();
|
||||
MacrosParser.populateEnv(env);
|
||||
|
||||
// Substitute passed-in variables
|
||||
for (const varName in env) {
|
||||
if (!Object.hasOwn(env, varName)) continue;
|
||||
|
||||
const param = env[varName];
|
||||
content = content.replace(new RegExp(`{{${varName}}}`, 'gi'), param);
|
||||
content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => {
|
||||
const param = env[varName];
|
||||
const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));
|
||||
|
@ -3,11 +3,9 @@ import {
|
||||
characters,
|
||||
chat,
|
||||
chat_metadata,
|
||||
crop_data,
|
||||
default_avatar,
|
||||
eventSource,
|
||||
event_types,
|
||||
getCropPopup,
|
||||
getRequestHeaders,
|
||||
getThumbnailUrl,
|
||||
name1,
|
||||
@ -24,6 +22,7 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { POPUP_TYPE, Popup } from './popup.js';
|
||||
|
||||
let savePersonasPage = 0;
|
||||
const GRID_STORAGE_KEY = 'Personas_GridView';
|
||||
@ -278,13 +277,15 @@ async function changeUserAvatar(e) {
|
||||
let url = '/api/avatars/upload';
|
||||
|
||||
if (!power_user.never_resize_avatars) {
|
||||
const confirmation = await callPopup(getCropPopup(dataUrl), 'avatarToCrop', '', { okButton: 'Crop', large: true, wide: true });
|
||||
if (!confirmation) {
|
||||
const dlg = new Popup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropImage: dataUrl });
|
||||
const result = await dlg.show();
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (crop_data !== undefined) {
|
||||
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
|
||||
if (dlg.cropData !== undefined) {
|
||||
url += `?crop=${encodeURIComponent(JSON.stringify(dlg.cropData))}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -589,7 +590,37 @@ function selectCurrentPersona() {
|
||||
}
|
||||
}
|
||||
|
||||
async function lockUserNameToChat() {
|
||||
/**
|
||||
* Checks if the persona is locked for the current chat.
|
||||
* @returns {boolean} Whether the persona is locked
|
||||
*/
|
||||
function isPersonaLocked() {
|
||||
return !!chat_metadata['persona'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks or unlocks the persona for the current chat.
|
||||
* @param {boolean} state Desired lock state
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function setPersonaLockState(state) {
|
||||
return state ? await lockPersona() : await unlockPersona();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the persona lock state for the current chat.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function togglePersonaLock() {
|
||||
return isPersonaLocked()
|
||||
? await unlockPersona()
|
||||
: await lockPersona();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock the persona for the current chat.
|
||||
*/
|
||||
async function unlockPersona() {
|
||||
if (chat_metadata['persona']) {
|
||||
console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`);
|
||||
delete chat_metadata['persona'];
|
||||
@ -598,9 +629,13 @@ async function lockUserNameToChat() {
|
||||
toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked');
|
||||
}
|
||||
updateUserLockIcon();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the persona for the current chat.
|
||||
*/
|
||||
async function lockPersona() {
|
||||
if (!(user_avatar in power_user.personas)) {
|
||||
console.log(`Creating a new persona ${user_avatar}`);
|
||||
if (power_user.persona_show_notifications) {
|
||||
@ -624,6 +659,7 @@ async function lockUserNameToChat() {
|
||||
updateUserLockIcon();
|
||||
}
|
||||
|
||||
|
||||
async function deleteUserAvatar(e) {
|
||||
e?.stopPropagation();
|
||||
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
|
||||
@ -972,7 +1008,7 @@ export function initPersonas() {
|
||||
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
|
||||
$(document).on('click', '.set_default_persona', setDefaultPersona);
|
||||
$(document).on('click', '.delete_avatar', deleteUserAvatar);
|
||||
$('#lock_user_name').on('click', lockUserNameToChat);
|
||||
$('#lock_user_name').on('click', togglePersonaLock);
|
||||
$('#create_dummy_persona').on('click', createDummyPersona);
|
||||
$('#persona_description').on('input', onPersonaDescriptionInput);
|
||||
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { power_user } from './power-user.js';
|
||||
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
|
||||
|
||||
/** @readonly */
|
||||
@ -11,6 +12,8 @@ export const POPUP_TYPE = {
|
||||
INPUT: 3,
|
||||
/** Popup without any button controls. Used to simply display content, with a small X in the corner. */
|
||||
DISPLAY: 4,
|
||||
/** Popup that displays an image to crop. Returns a cropped image in result. */
|
||||
CROP: 5,
|
||||
};
|
||||
|
||||
/** @readonly */
|
||||
@ -36,12 +39,14 @@ export const POPUP_RESULT = {
|
||||
* @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
|
||||
* @property {(popup: Popup) => boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close
|
||||
* @property {(popup: Popup) => void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up
|
||||
* @property {number?} [cropAspect=null] - Aspect ratio for the crop popup
|
||||
* @property {string?} [cropImage=null] - Image URL to display in the crop popup
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CustomPopupButton
|
||||
* @property {string} text - The text of the button
|
||||
* @property {POPUP_RESULT|number?} result - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
|
||||
* @property {POPUP_RESULT|number?} [result] - The result of the button - can also be a custom result value to make be able to find out that this button was clicked. If no result is specified, this button will **not** close the popup.
|
||||
* @property {string[]|string?} [classes] - Optional custom CSS classes applied to the button
|
||||
* @property {()=>void?} [action] - Optional action to perform when the button is clicked
|
||||
* @property {boolean?} [appendAtEnd] - Whether to append the button to the end of the popup - by default it will be prepended
|
||||
@ -69,6 +74,22 @@ const showPopupHelper = {
|
||||
const value = await popup.show();
|
||||
return value ? String(value) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Asynchronously displays a confirmation popup with the given header and text, returning the clicked result button value.
|
||||
*
|
||||
* @param {string} header - The header text for the popup.
|
||||
* @param {string} text - The main text for the popup.
|
||||
* @param {PopupOptions} [popupOptions={}] - Options for the popup.
|
||||
* @return {Promise<POPUP_RESULT>} A Promise that resolves with the result of the user's interaction.
|
||||
*/
|
||||
confirm: async (header, text, popupOptions = {}) => {
|
||||
const content = PopupUtils.BuildTextWithHeader(header, text);
|
||||
const popup = new Popup(content, POPUP_TYPE.CONFIRM, null, popupOptions);
|
||||
const result = await popup.show();
|
||||
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
export class Popup {
|
||||
@ -84,6 +105,8 @@ export class Popup {
|
||||
/** @type {HTMLElement} */ okButton;
|
||||
/** @type {HTMLElement} */ cancelButton;
|
||||
/** @type {HTMLElement} */ closeButton;
|
||||
/** @type {HTMLElement} */ cropWrap;
|
||||
/** @type {HTMLImageElement} */ cropImage;
|
||||
/** @type {POPUP_RESULT|number?} */ defaultResult;
|
||||
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
|
||||
|
||||
@ -92,6 +115,7 @@ export class Popup {
|
||||
|
||||
/** @type {POPUP_RESULT|number} */ result;
|
||||
/** @type {any} */ value;
|
||||
/** @type {any} */ cropData;
|
||||
|
||||
/** @type {HTMLElement} */ lastFocus;
|
||||
|
||||
@ -106,7 +130,7 @@ export class Popup {
|
||||
* @param {string} [inputValue=''] - The initial value of the input field
|
||||
* @param {PopupOptions} [options={}] - Additional options for the popup
|
||||
*/
|
||||
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) {
|
||||
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
|
||||
Popup.util.popups.push(this);
|
||||
|
||||
// Make this popup uniquely identifiable
|
||||
@ -128,6 +152,8 @@ export class Popup {
|
||||
this.okButton = this.dlg.querySelector('.popup-button-ok');
|
||||
this.cancelButton = this.dlg.querySelector('.popup-button-cancel');
|
||||
this.closeButton = this.dlg.querySelector('.popup-button-close');
|
||||
this.cropWrap = this.dlg.querySelector('.popup-crop-wrap');
|
||||
this.cropImage = this.dlg.querySelector('.popup-crop-image');
|
||||
|
||||
this.dlg.setAttribute('data-id', this.id);
|
||||
if (wide) this.dlg.classList.add('wide_dialogue_popup');
|
||||
@ -159,6 +185,10 @@ export class Popup {
|
||||
} else {
|
||||
this.controls.insertBefore(buttonElement, this.okButton);
|
||||
}
|
||||
|
||||
if (typeof button.action === 'function') {
|
||||
buttonElement.addEventListener('click', button.action);
|
||||
}
|
||||
});
|
||||
|
||||
// Set the default button class
|
||||
@ -169,6 +199,7 @@ export class Popup {
|
||||
// General styling for all types first, that might be overriden for specific types below
|
||||
this.input.style.display = 'none';
|
||||
this.closeButton.style.display = 'none';
|
||||
this.cropWrap.style.display = 'none';
|
||||
|
||||
switch (type) {
|
||||
case POPUP_TYPE.TEXT: {
|
||||
@ -190,6 +221,22 @@ export class Popup {
|
||||
this.closeButton.style.display = 'block';
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.CROP: {
|
||||
this.cropWrap.style.display = 'block';
|
||||
this.cropImage.src = cropImage;
|
||||
if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-crop');
|
||||
$(this.cropImage).cropper({
|
||||
aspectRatio: cropAspect ?? 2 / 3,
|
||||
autoCropArea: 1,
|
||||
viewMode: 2,
|
||||
rotatable: false,
|
||||
crop: (event) => {
|
||||
this.cropData = event.detail;
|
||||
this.cropData.want_resize = !power_user.never_resize_avatars;
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Unknown popup type.', type);
|
||||
break;
|
||||
@ -220,6 +267,7 @@ export class Popup {
|
||||
this.dlg.querySelectorAll('[data-result]').forEach(resultControl => {
|
||||
if (!(resultControl instanceof HTMLElement)) return;
|
||||
const result = Number(resultControl.dataset.result);
|
||||
if (String(undefined) === String(resultControl.dataset.result)) return;
|
||||
if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result);
|
||||
const type = resultControl.dataset.resultEvent || 'click';
|
||||
resultControl.addEventListener(type, () => this.complete(result));
|
||||
@ -347,6 +395,13 @@ export class Popup {
|
||||
else value = false; // Might a custom negative value?
|
||||
}
|
||||
|
||||
// Cropped image should be returned as a data URL
|
||||
if (this.type === POPUP_TYPE.CROP) {
|
||||
value = result >= POPUP_RESULT.AFFIRMATIVE
|
||||
? $(this.cropImage).data('cropper').getCroppedCanvas().toDataURL('image/jpeg')
|
||||
: null;
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.result = result;
|
||||
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
saveSettingsDebounced,
|
||||
scrollChatToBottom,
|
||||
characters,
|
||||
callPopup,
|
||||
reloadMarkdownProcessor,
|
||||
reloadCurrentChat,
|
||||
getRequestHeaders,
|
||||
@ -48,6 +47,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashComma
|
||||
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
|
||||
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
|
||||
export {
|
||||
loadPowerUserSettings,
|
||||
@ -1432,7 +1432,7 @@ export function registerDebugFunction(functionId, name, description, func) {
|
||||
|
||||
async function showDebugMenu() {
|
||||
const template = await renderTemplateAsync('debug', { functions: debug_functions });
|
||||
callPopup(template, 'text', '', { wide: true, large: true });
|
||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
|
||||
}
|
||||
|
||||
switchUiMode();
|
||||
@ -2207,7 +2207,8 @@ async function deleteTheme() {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callPopup(`Are you sure you want to delete the theme "${themeName}"?`, 'confirm', '', { okButton: 'Yes' });
|
||||
const template = $(await renderTemplateAsync('themeDelete', { themeName }));
|
||||
const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
@ -2269,7 +2270,8 @@ async function importTheme(file) {
|
||||
}
|
||||
|
||||
if (typeof parsed.custom_css === 'string' && parsed.custom_css.includes('@import')) {
|
||||
const confirm = await callPopup('This theme contains @import lines in the Custom CSS. Press "Yes" to proceed.', 'confirm', '', { okButton: 'Yes' });
|
||||
const template = $(await renderTemplateAsync('themeImportWarning'));
|
||||
const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
|
||||
if (!confirm) {
|
||||
throw new Error('Theme contains @import lines');
|
||||
}
|
||||
@ -2294,11 +2296,13 @@ async function importTheme(file) {
|
||||
*/
|
||||
async function saveTheme(name = undefined, theme = undefined) {
|
||||
if (typeof name !== 'string') {
|
||||
name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
|
||||
const newName = await callGenericPopup('Enter a theme preset name:', POPUP_TYPE.INPUT, power_user.theme);
|
||||
|
||||
if (!name) {
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
name = String(newName);
|
||||
}
|
||||
|
||||
if (typeof theme !== 'object') {
|
||||
@ -2396,7 +2400,7 @@ function getNewTheme(parsed) {
|
||||
}
|
||||
|
||||
async function saveMovingUI() {
|
||||
const name = await callPopup('Enter a name for the MovingUI Preset:', 'input');
|
||||
const name = await callGenericPopup('Enter a name for the MovingUI Preset:', POPUP_TYPE.INPUT);
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
|
@ -46,7 +46,7 @@ import { extension_settings, getContext, saveMetadataDebounced } from './extensi
|
||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||
import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
|
||||
import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
|
||||
import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, user_avatar } from './personas.js';
|
||||
import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
|
||||
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||||
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
|
||||
@ -119,9 +119,18 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'lock',
|
||||
callback: bindCallback,
|
||||
callback: lockPersonaCallback,
|
||||
aliases: ['bind'],
|
||||
helpString: 'Locks/unlocks a persona (name and avatar) to the current chat',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'state',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: true,
|
||||
defaultValue: 'toggle',
|
||||
enumProvider: commonEnumProviders.boolean('onOffToggle'),
|
||||
}),
|
||||
],
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'bg',
|
||||
@ -2003,6 +2012,9 @@ async function addSwipeCallback(_, arg) {
|
||||
lastMessage.swipe_info = [{}];
|
||||
lastMessage.swipe_id = 0;
|
||||
}
|
||||
if (!Array.isArray(lastMessage.swipe_info)) {
|
||||
lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
|
||||
}
|
||||
|
||||
lastMessage.swipes.push(arg);
|
||||
lastMessage.swipe_info.push({
|
||||
@ -2578,8 +2590,23 @@ function syncCallback() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function bindCallback() {
|
||||
$('#lock_user_name').trigger('click');
|
||||
async function lockPersonaCallback(_args, value) {
|
||||
if (['toggle', 't', ''].includes(value.trim().toLowerCase())) {
|
||||
await togglePersonaLock();
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isTrueBoolean(value)) {
|
||||
await setPersonaLockState(true);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isFalseBoolean(value)) {
|
||||
await setPersonaLockState(false);
|
||||
return '';
|
||||
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -2705,9 +2732,26 @@ export async function sendMessageAs(args, text) {
|
||||
bias: bias.trim().length ? bias : null,
|
||||
gen_id: Date.now(),
|
||||
isSmallSys: compact,
|
||||
api: 'manual',
|
||||
model: 'slash command',
|
||||
},
|
||||
};
|
||||
|
||||
message.swipe_id = 0;
|
||||
message.swipes = [message.mes];
|
||||
message.swipes_info = [{
|
||||
send_date: message.send_date,
|
||||
gen_started: null,
|
||||
gen_finished: null,
|
||||
extra: {
|
||||
bias: message.extra.bias,
|
||||
gen_id: message.extra.gen_id,
|
||||
isSmallSys: compact,
|
||||
api: 'manual',
|
||||
model: 'slash command',
|
||||
},
|
||||
}];
|
||||
|
||||
const insertAt = Number(resolveVariable(args.at));
|
||||
|
||||
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
|
||||
@ -2750,6 +2794,8 @@ export async function sendNarratorMessage(args, text) {
|
||||
bias: bias.trim().length ? bias : null,
|
||||
gen_id: Date.now(),
|
||||
isSmallSys: compact,
|
||||
api: 'manual',
|
||||
model: 'slash command',
|
||||
},
|
||||
};
|
||||
|
||||
@ -2800,6 +2846,8 @@ export async function promptQuietForLoudResponse(who, text) {
|
||||
extra: {
|
||||
type: system_message_types.COMMENT,
|
||||
gen_id: Date.now(),
|
||||
api: 'manual',
|
||||
model: 'slash command',
|
||||
},
|
||||
};
|
||||
|
||||
@ -2828,6 +2876,8 @@ async function sendCommentMessage(args, text) {
|
||||
type: system_message_types.COMMENT,
|
||||
gen_id: Date.now(),
|
||||
isSmallSys: compact,
|
||||
api: 'manual',
|
||||
model: 'slash command',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<h3>Import Tags For {{charName}}</h3>
|
||||
<h3><span data-i18n="Import Tags For _begin">Import Tags For </span>{{charName}}<span data-i18n="Import Tags For _end"></span></h3>
|
||||
<div class="import_avatar_placeholder"></div>
|
||||
<div class="import_tags_content justifyLeft">
|
||||
<small data-i18n="Click remove on any tag to remove it from this import.<br />Select one of the import options to finish importing the tags.">
|
||||
@ -23,10 +23,10 @@
|
||||
<small>
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-3" for="import_remember_option">
|
||||
<input type="checkbox" id="import_remember_option" name="import_remember_option" />
|
||||
<span data-i18n="Remember my choice">
|
||||
Remember my choice
|
||||
<span>
|
||||
<span data-i18n="Remember my choice">Remember my choice</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p"
|
||||
data-i18n="[title]Remember the chosen import option
If anything besides 'Cancel' is selected, this dialog will not show up anymore.
To change this, go to the settings and modify "Tag Import Option".

If the "Import" option is chosen, the global setting will stay on "Ask"."
|
||||
data-i18n="[title]Remember the chosen import option If anything besides 'Cancel' is selected, this dialog will not show up anymore. To change this, go to the settings and modify "Tag Import Option". If the "Import" option is chosen, the global setting will stay on "Ask"."
|
||||
title="Remember the chosen import option
If anything besides 'Cancel' is selected, this dialog will not show up anymore.
To change this, go to the settings and modify "Tag Import Option".

If the "Import" option is chosen, the global setting will stay on "Ask".">
|
||||
</div>
|
||||
</span>
|
||||
|
@ -1,25 +1,27 @@
|
||||
<h3 data-i18n="Debug Menu">Debug Menu</h3>
|
||||
<div data-i18n="Debug Warning">
|
||||
Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.
|
||||
</div>
|
||||
<table id="debug_table" class="responsiveTable">
|
||||
{{#each functions}}
|
||||
{{#with this}}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="justifyLeft">
|
||||
<b>{{this.name}}</b>
|
||||
</div>
|
||||
<div class="justifyLeft">
|
||||
{{this.description}}
|
||||
</div>
|
||||
<div class="flex-container justifyCenter">
|
||||
<div class="menu_button menu_button_icon" data-debug-function="{{this.functionId}}" data-i18n="Execute">
|
||||
Execute
|
||||
<div>
|
||||
<h3 data-i18n="Debug Menu">Debug Menu</h3>
|
||||
<div data-i18n="Debug Warning">
|
||||
Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.
|
||||
</div>
|
||||
<table id="debug_table" class="responsiveTable">
|
||||
{{#each functions}}
|
||||
{{#with this}}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="justifyLeft">
|
||||
<b>{{this.name}}</b>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
</table>
|
||||
<div class="justifyLeft">
|
||||
{{this.description}}
|
||||
</div>
|
||||
<div class="flex-container justifyCenter">
|
||||
<div class="menu_button menu_button_icon" data-debug-function="{{this.functionId}}" data-i18n="Execute">
|
||||
Execute
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
|
5
public/scripts/templates/duplicateConfirm.html
Normal file
5
public/scripts/templates/duplicateConfirm.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<h3>Are you sure you want to duplicate this character?</h3>
|
||||
<span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span>
|
||||
<br>
|
||||
</div>
|
24
public/scripts/templates/forbidMedia.html
Normal file
24
public/scripts/templates/forbidMedia.html
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="forbid_media_override flex-container flexFlowColumn">
|
||||
<h4 data-i18n="Forbid Media Override explanation" class="margin0">
|
||||
Ability of the current character/group to use external media in chats.
|
||||
</h4>
|
||||
<small data-i18n="Forbid Media Override subtitle" class="marginBot5">
|
||||
Media: images, videos, audio. External: not hosted on the local server.
|
||||
</small>
|
||||
<label class="checkbox_label" for="forbid_media_override_global">
|
||||
<input type="radio" id="forbid_media_override_global" name="forbid_media_override" />
|
||||
<span>
|
||||
<span data-i18n="Use global setting">Use global setting</span>
|
||||
<b class="forbid_media_global_state_forbidden">(forbidden)</b>
|
||||
<b class="forbid_media_global_state_allowed">(allowed)</b>
|
||||
</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_media_override_forbidden">
|
||||
<input type="radio" id="forbid_media_override_forbidden" name="forbid_media_override" />
|
||||
<span data-i18n="Always forbidden">Always forbidden</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_media_override_allowed">
|
||||
<input type="radio" id="forbid_media_override_allowed" name="forbid_media_override" />
|
||||
<span data-i18n="Always allowed">Always allowed</span>
|
||||
</label>
|
||||
</div>
|
@ -127,3 +127,6 @@ API Used: {{this_main_api}}<br>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="rawPromptPopup" class="list-group">
|
||||
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
|
||||
</div>
|
||||
|
@ -107,3 +107,6 @@ API Used: {{this_main_api}}<br>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="rawPromptPopup" class="list-group">
|
||||
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
|
||||
</div>
|
||||
|
3
public/scripts/templates/themeDelete.html
Normal file
3
public/scripts/templates/themeDelete.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
Are you sure you want to delete the theme "{{themeName}}"?
|
||||
</div>
|
3
public/scripts/templates/themeImportWarning.html
Normal file
3
public/scripts/templates/themeImportWarning.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
This theme contains @import lines in the Custom CSS. Press "Yes" to proceed.
|
||||
</div>
|
@ -1,16 +1,16 @@
|
||||
<div id="extensionsMenu" class="options-content" style="display: none;">
|
||||
<div id="caption_wand_container" class="extension_container"></div>
|
||||
<div id="data_bank_wand_container" class="extension_container"></div>
|
||||
<div id="attach_file_wand_container" class="extension_container"></div>
|
||||
<div id="sd_wand_container" class="extension_container"></div>
|
||||
<div id="caption_wand_container" class="extension_container"></div>
|
||||
<div id="tts_wand_container" class="extension_container"></div>
|
||||
<div id="screen_share_wand_container" class="extension_container"></div>
|
||||
<div id="prompt_inspector_wand_container" class="extension_container"></div>
|
||||
<div id="emulatorjs_wand_container" class="extension_container"></div>
|
||||
<div id="notebook_wand_container" class="extension_container"></div>
|
||||
<div id="chess_wand_container" class="extension_container"></div>
|
||||
<div id="screen_share_wand_container" class="extension_container"></div>
|
||||
<div id="translate_wand_container" class="extension_container"></div>
|
||||
<div id="token_counter_wand_container" class="extension_container"></div>
|
||||
<div id="dice_wand_container" class="extension_container"></div>
|
||||
<div id="objective_wand_container" class="extension_container"></div>
|
||||
<div id="token_counter_wand_container" class="extension_container"></div>
|
||||
<div id="prompt_inspector_wand_container" class="extension_container"></div>
|
||||
<div id="translate_wand_container" class="extension_container"></div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { callPopup, getCropPopup, getRequestHeaders } from '../script.js';
|
||||
import { getRequestHeaders } from '../script.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
|
||||
@ -592,7 +592,7 @@ async function viewSettingsSnapshots() {
|
||||
}
|
||||
}
|
||||
|
||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false });
|
||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: false, large: false, allowVerticalScrolling: true });
|
||||
template.find('.makeSnapshotButton').on('click', () => makeSnapshot(renderSnapshots));
|
||||
renderSnapshots();
|
||||
}
|
||||
@ -727,14 +727,14 @@ async function openUserProfile() {
|
||||
*/
|
||||
async function cropAndUploadAvatar(handle, file) {
|
||||
const dataUrl = await getBase64Async(await ensureImageFormatSupported(file));
|
||||
const croppedImage = await callPopup(getCropPopup(dataUrl), 'avatarToCrop', '', { cropAspect: 1 });
|
||||
const croppedImage = await callGenericPopup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropAspect: 1, cropImage: dataUrl });
|
||||
if (!croppedImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await changeAvatar(handle, String(croppedImage));
|
||||
|
||||
return croppedImage;
|
||||
return String(croppedImage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { getContext } from './extensions.js';
|
||||
import { callPopup, getRequestHeaders } from '../script.js';
|
||||
import { getRequestHeaders } from '../script.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { collapseNewlines } from './power-user.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { Popup } from './popup.js';
|
||||
|
||||
/**
|
||||
* Pagination status string template.
|
||||
@ -1821,7 +1822,7 @@ export async function checkOverwriteExistingData(type, existingNames, name, { in
|
||||
return true;
|
||||
}
|
||||
|
||||
const overwrite = interactive ? await callPopup(`<h3>${type} ${actionName}</h3><p>A ${type.toLowerCase()} with the same name already exists:<br />${existing}</p>Do you want to overwrite it?`, 'confirm') : false;
|
||||
const overwrite = interactive && await Popup.show.confirm(`${type} ${actionName}`, `<p>A ${type.toLowerCase()} with the same name already exists:<br />${existing}</p>Do you want to overwrite it?`);
|
||||
if (!overwrite) {
|
||||
toastr.warning(`${type} ${actionName.toLowerCase()} cancelled. A ${type.toLowerCase()} with the same name already exists:<br />${existing}`, `${type} ${actionName}`, { escapeHtml: false });
|
||||
return false;
|
||||
|
@ -16,6 +16,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
|
||||
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
|
||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||
import { Popup } from './popup.js';
|
||||
|
||||
export {
|
||||
world_info,
|
||||
@ -98,6 +99,7 @@ const MAX_SCAN_DEPTH = 1000;
|
||||
* @property {number} [selectiveLogic] The logic to use for selective activation
|
||||
* @property {number} [sticky] The sticky value of the entry
|
||||
* @property {number} [cooldown] The cooldown of the entry
|
||||
* @property {number} [delay] The delay of the entry
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -110,7 +112,7 @@ const MAX_SCAN_DEPTH = 1000;
|
||||
|
||||
/**
|
||||
* @typedef TimedEffectType Type of timed effect
|
||||
* @type {'sticky'|'cooldown'}
|
||||
* @type {'sticky'|'cooldown'|'delay'}
|
||||
*/
|
||||
// End typedef area
|
||||
|
||||
@ -370,6 +372,7 @@ class WorldInfoTimedEffects {
|
||||
#buffer = {
|
||||
'sticky': [],
|
||||
'cooldown': [],
|
||||
'delay': [],
|
||||
};
|
||||
|
||||
/**
|
||||
@ -403,6 +406,8 @@ class WorldInfoTimedEffects {
|
||||
'cooldown': (entry) => {
|
||||
console.debug('Cooldown ended for entry', entry.uid);
|
||||
},
|
||||
|
||||
'delay': () => {},
|
||||
};
|
||||
|
||||
/**
|
||||
@ -528,12 +533,31 @@ class WorldInfoTimedEffects {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes entries for the "delay" timed effect.
|
||||
* @param {WIScanEntry[]} buffer Buffer to store the entries
|
||||
*/
|
||||
#checkDelayEffect(buffer) {
|
||||
for (const entry of this.#entries) {
|
||||
if (!entry.delay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.#chat.length < entry.delay) {
|
||||
buffer.push(entry);
|
||||
console.log('Timed effect "delay" applied to entry', entry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for timed effects on chat messages.
|
||||
*/
|
||||
checkTimedEffects() {
|
||||
this.#checkTimedEffectOfType('sticky', this.#buffer.sticky, this.#onEnded.sticky.bind(this));
|
||||
this.#checkTimedEffectOfType('cooldown', this.#buffer.cooldown, this.#onEnded.cooldown.bind(this));
|
||||
this.#checkDelayEffect(this.#buffer.delay);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -610,7 +634,7 @@ class WorldInfoTimedEffects {
|
||||
* @returns {boolean} Is recognized type
|
||||
*/
|
||||
isValidEffectType(type) {
|
||||
return typeof type === 'string' && ['sticky', 'cooldown'].includes(type.trim().toLowerCase());
|
||||
return typeof type === 'string' && ['sticky', 'cooldown', 'delay'].includes(type.trim().toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1684,8 +1708,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
|
||||
// Regardless of whether success is displayed or not. Make sure the delete button is available.
|
||||
// Do not put this code behind.
|
||||
$('#world_popup_delete').off('click').on('click', async () => {
|
||||
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, 'confirm');
|
||||
|
||||
const confirmation = await Popup.show.confirm(`Delete the World/Lorebook: "${name}"?`, 'This action is irreversible!');
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
@ -1733,14 +1756,21 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
|
||||
return entriesArray;
|
||||
}
|
||||
|
||||
const storageKey = 'WI_PerPage';
|
||||
const perPageDefault = 25;
|
||||
let startPage = 1;
|
||||
|
||||
if (navigation === navigation_option.previous) {
|
||||
startPage = $('#world_info_pagination').pagination('getCurrentPageNum');
|
||||
}
|
||||
|
||||
const storageKey = 'WI_PerPage';
|
||||
const perPageDefault = 25;
|
||||
if (typeof navigation === 'number' && Number(navigation) >= 0) {
|
||||
const data = getDataArray();
|
||||
const uidIndex = data.findIndex(x => x.uid === navigation);
|
||||
const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;
|
||||
startPage = Math.floor(uidIndex / perPage) + 1;
|
||||
}
|
||||
|
||||
$('#world_info_pagination').pagination({
|
||||
dataSource: getDataArray,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
|
||||
@ -1801,15 +1831,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (typeof navigation === 'number' && Number(navigation) >= 0) {
|
||||
const selector = `#world_popup_entries_list [uid="${navigation}"]`;
|
||||
const data = getDataArray();
|
||||
const uidIndex = data.findIndex(x => x.uid === navigation);
|
||||
const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault;
|
||||
const page = Math.floor(uidIndex / perPage) + 1;
|
||||
$('#world_info_pagination').pagination('go', page);
|
||||
waitUntilCondition(() => document.querySelector(selector) !== null).finally(() => {
|
||||
const element = $(selector);
|
||||
|
||||
@ -1862,7 +1885,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
|
||||
|
||||
$('#world_duplicate').off('click').on('click', async () => {
|
||||
const tempName = getFreeWorldName();
|
||||
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
|
||||
const finalName = await Popup.show.input('Create a new World Info?', 'Enter a name for the new file:', tempName);
|
||||
|
||||
if (finalName) {
|
||||
await saveWorldInfo(finalName, data, true);
|
||||
@ -1941,6 +1964,7 @@ const originalDataKeyMap = {
|
||||
'groupWeight': 'extensions.group_weight',
|
||||
'sticky': 'extensions.sticky',
|
||||
'cooldown': 'extensions.cooldown',
|
||||
'delay': 'extensions.delay',
|
||||
};
|
||||
|
||||
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
|
||||
@ -2589,6 +2613,19 @@ function getWorldEntry(name, data, entry) {
|
||||
});
|
||||
cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input');
|
||||
|
||||
// delay
|
||||
const delay = template.find('input[name="delay"]');
|
||||
delay.data('uid', entry.uid);
|
||||
delay.on('input', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const value = Number($(this).val());
|
||||
data.entries[uid].delay = !isNaN(value) ? value : null;
|
||||
|
||||
setOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
delay.val(entry.delay > 0 ? entry.delay : '').trigger('input');
|
||||
|
||||
// probability
|
||||
if (entry.probability === undefined) {
|
||||
entry.probability = null;
|
||||
@ -3139,6 +3176,7 @@ const newEntryDefinition = {
|
||||
role: { default: 0, type: 'enum' },
|
||||
sticky: { default: null, type: 'number?' },
|
||||
cooldown: { default: null, type: 'number?' },
|
||||
delay: { default: null, type: 'number?' },
|
||||
};
|
||||
|
||||
const newEntryTemplate = Object.fromEntries(
|
||||
@ -3190,7 +3228,7 @@ async function saveWorldInfo(name, data, immediately) {
|
||||
|
||||
async function renameWorldInfo(name, data) {
|
||||
const oldName = name;
|
||||
const newName = await callPopup('<h3>Rename World Info</h3>Enter a new name:', 'input', oldName);
|
||||
const newName = await Popup.show.input('Rename World Info', 'Enter a new name:', oldName);
|
||||
|
||||
if (oldName === newName || !newName) {
|
||||
console.debug('World info rename cancelled');
|
||||
@ -3533,9 +3571,15 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
|
||||
|
||||
const isSticky = timedEffects.isEffectActive('sticky', entry);
|
||||
const isCooldown = timedEffects.isEffectActive('cooldown', entry);
|
||||
const isDelay = timedEffects.isEffectActive('delay', entry);
|
||||
|
||||
if (isDelay) {
|
||||
console.debug(`WI entry ${entry.uid} suppressed by delay`, entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCooldown && !isSticky) {
|
||||
console.debug(`WI entry ${entry.uid} suppressed by cooldown`);
|
||||
console.debug(`WI entry ${entry.uid} suppressed by cooldown`, entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -3933,6 +3977,7 @@ function convertAgnaiMemoryBook(inputObj) {
|
||||
role: extension_prompt_roles.SYSTEM,
|
||||
sticky: null,
|
||||
cooldown: null,
|
||||
delay: null,
|
||||
};
|
||||
});
|
||||
|
||||
@ -3974,6 +4019,7 @@ function convertRisuLorebook(inputObj) {
|
||||
role: extension_prompt_roles.SYSTEM,
|
||||
sticky: null,
|
||||
cooldown: null,
|
||||
delay: null,
|
||||
};
|
||||
});
|
||||
|
||||
@ -4020,6 +4066,7 @@ function convertNovelLorebook(inputObj) {
|
||||
role: extension_prompt_roles.SYSTEM,
|
||||
sticky: null,
|
||||
cooldown: null,
|
||||
delay: null,
|
||||
};
|
||||
});
|
||||
|
||||
@ -4068,6 +4115,7 @@ function convertCharacterBook(characterBook) {
|
||||
vectorized: entry.extensions?.vectorized ?? false,
|
||||
sticky: entry.extensions?.sticky ?? null,
|
||||
cooldown: entry.extensions?.cooldown ?? null,
|
||||
delay: entry.extensions?.delay ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
@ -4138,11 +4186,9 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
|
||||
}
|
||||
|
||||
const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
|
||||
const confirmationText = (`<h3>Are you sure you want to import "${bookName}"?</h3>`) + (world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
|
||||
|
||||
if (!skipPopup) {
|
||||
const confirmation = await callPopup(confirmationText, 'confirm');
|
||||
|
||||
const confirmation = await Popup.show.confirm(`Are you sure you want to import "${bookName}"?`, world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
@ -4382,7 +4428,7 @@ jQuery(() => {
|
||||
|
||||
$('#world_create_button').on('click', async () => {
|
||||
const tempName = getFreeWorldName();
|
||||
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
|
||||
const finalName = await Popup.show.input('Create a new World Info', 'Enter a name for the new file:', tempName);
|
||||
|
||||
if (finalName) {
|
||||
await createNewWorldInfo(finalName, { interactive: true });
|
||||
|
@ -82,7 +82,7 @@
|
||||
/*base variable calculated in rems*/
|
||||
--fontScale: 1;
|
||||
--mainFontSize: calc(var(--fontScale) * 15px);
|
||||
--mainFontFamily: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||
--mainFontFamily: "Noto Sans", sans-serif;
|
||||
--monoFontFamily: 'Noto Sans Mono', 'Courier New', Consolas, monospace;
|
||||
|
||||
/* base variable for blur strength slider calculations */
|
||||
@ -256,10 +256,6 @@ input[type='checkbox']:focus-visible {
|
||||
color: var(--SmartThemeEmColor);
|
||||
}
|
||||
|
||||
#rawPromptWrapper {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tokenGraph {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
@ -462,7 +458,7 @@ code {
|
||||
kbd {
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
font-family: Consolas, monospace;
|
||||
font-family: var(--monoFontFamily);
|
||||
white-space: nowrap;
|
||||
/* background-color: #eeeeee; */
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
@ -4188,13 +4184,14 @@ h5 {
|
||||
grid-template-columns: 340px auto;
|
||||
}
|
||||
|
||||
#avatarCropWrap {
|
||||
.popup-crop-wrap {
|
||||
margin: 10px auto;
|
||||
max-height: 90%;
|
||||
max-height: 75vh;
|
||||
max-height: 75svh;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#avatarToCrop {
|
||||
.popup-crop-wrap img {
|
||||
max-width: 100%;
|
||||
/* This rule is very important, please do not ignore this! */
|
||||
}
|
||||
@ -4400,8 +4397,7 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#export_format_popup,
|
||||
#rawPromptPopup {
|
||||
#export_format_popup {
|
||||
display: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
@ -4409,7 +4405,7 @@ a {
|
||||
#rawPromptPopup {
|
||||
inset: 0px auto auto 0px;
|
||||
margin: 0px;
|
||||
transform: translate(909px, 47px);
|
||||
transform: translate(500px, 0px);
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
@ -4428,7 +4424,8 @@ a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#rawPopupWrapper {
|
||||
#rawPromptWrapper {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
|
10
server.js
10
server.js
@ -136,7 +136,7 @@ const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProte
|
||||
const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH);
|
||||
const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS);
|
||||
|
||||
const { UPLOADS_PATH } = require('./src/constants');
|
||||
const uploadsPath = path.join(dataRoot, require('./src/constants').UPLOADS_DIRECTORY);
|
||||
|
||||
// CORS Settings //
|
||||
const CORS = cors({
|
||||
@ -286,7 +286,7 @@ app.use(userModule.requireLoginMiddleware);
|
||||
app.get('/api/ping', (_, response) => response.sendStatus(204));
|
||||
|
||||
// File uploads
|
||||
app.use(multer({ dest: UPLOADS_PATH, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
|
||||
app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
|
||||
app.use(require('./src/middleware/multerMonkeyPatch'));
|
||||
|
||||
// User data mount
|
||||
@ -303,8 +303,8 @@ app.get('/version', async function (_, response) {
|
||||
|
||||
function cleanUploads() {
|
||||
try {
|
||||
if (fs.existsSync(UPLOADS_PATH)) {
|
||||
const uploads = fs.readdirSync(UPLOADS_PATH);
|
||||
if (fs.existsSync(uploadsPath)) {
|
||||
const uploads = fs.readdirSync(uploadsPath);
|
||||
|
||||
if (!uploads.length) {
|
||||
return;
|
||||
@ -312,7 +312,7 @@ function cleanUploads() {
|
||||
|
||||
console.debug(`Cleaning uploads folder (${uploads.length} files)`);
|
||||
uploads.forEach(file => {
|
||||
const pathToFile = path.join(UPLOADS_PATH, file);
|
||||
const pathToFile = path.join(uploadsPath, file);
|
||||
fs.unlinkSync(pathToFile);
|
||||
});
|
||||
}
|
||||
|
@ -196,7 +196,10 @@ const CHAT_COMPLETION_SOURCES = {
|
||||
GROQ: 'groq',
|
||||
};
|
||||
|
||||
const UPLOADS_PATH = './uploads';
|
||||
/**
|
||||
* Path to multer file uploads under the data root.
|
||||
*/
|
||||
const UPLOADS_DIRECTORY = '_uploads';
|
||||
|
||||
// TODO: this is copied from the client code; there should be a way to de-duplicate it eventually
|
||||
const TEXTGEN_TYPES = {
|
||||
@ -364,7 +367,7 @@ module.exports = {
|
||||
PUBLIC_DIRECTORIES,
|
||||
USER_DIRECTORY_TEMPLATE,
|
||||
UNSAFE_EXTENSIONS,
|
||||
UPLOADS_PATH,
|
||||
UPLOADS_DIRECTORY,
|
||||
GEMINI_SAFETY,
|
||||
BISON_SAFETY,
|
||||
TEXTGEN_TYPES,
|
||||
|
@ -4,7 +4,7 @@ const fs = require('fs');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { AVATAR_WIDTH, AVATAR_HEIGHT, UPLOADS_PATH } = require('../constants');
|
||||
const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
|
||||
const { getImages, tryParse } = require('../util');
|
||||
|
||||
// image processing related library imports
|
||||
@ -39,7 +39,7 @@ router.post('/upload', urlencodedParser, async (request, response) => {
|
||||
if (!request.file) return response.sendStatus(400);
|
||||
|
||||
try {
|
||||
const pathToUpload = path.join(UPLOADS_PATH, request.file.filename);
|
||||
const pathToUpload = path.join(request.file.destination, request.file.filename);
|
||||
const crop = tryParse(request.query.crop);
|
||||
let rawImg = await jimp.read(pathToUpload);
|
||||
|
||||
|
@ -4,7 +4,6 @@ const express = require('express');
|
||||
const sanitize = require('sanitize-filename');
|
||||
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { UPLOADS_PATH } = require('../constants');
|
||||
const { invalidateThumbnail } = require('./thumbnails');
|
||||
const { getImages } = require('../util');
|
||||
|
||||
@ -60,7 +59,7 @@ router.post('/rename', jsonParser, function (request, response) {
|
||||
router.post('/upload', urlencodedParser, function (request, response) {
|
||||
if (!request.body || !request.file) return response.sendStatus(400);
|
||||
|
||||
const img_path = path.join(UPLOADS_PATH, request.file.filename);
|
||||
const img_path = path.join(request.file.destination, request.file.filename);
|
||||
const filename = request.file.originalname;
|
||||
|
||||
try {
|
||||
|
@ -11,7 +11,7 @@ const mime = require('mime-types');
|
||||
|
||||
const jimp = require('jimp');
|
||||
|
||||
const { UPLOADS_PATH, AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
|
||||
const { AVATAR_WIDTH, AVATAR_HEIGHT } = require('../constants');
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer } = require('../util');
|
||||
const { TavernCardValidator } = require('../validator/TavernCardValidator');
|
||||
@ -485,6 +485,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
vectorized: entry.vectorized ?? false,
|
||||
sticky: entry.sticky ?? null,
|
||||
cooldown: entry.cooldown ?? null,
|
||||
delay: entry.delay ?? null,
|
||||
},
|
||||
};
|
||||
|
||||
@ -729,7 +730,7 @@ router.post('/create', urlencodedParser, async function (request, response) {
|
||||
return response.send(avatarName);
|
||||
} else {
|
||||
const crop = tryParse(request.query.crop);
|
||||
const uploadPath = path.join(UPLOADS_PATH, request.file.filename);
|
||||
const uploadPath = path.join(request.file.destination, request.file.filename);
|
||||
await writeCharacterData(uploadPath, char, internalName, request, crop);
|
||||
fs.unlinkSync(uploadPath);
|
||||
return response.send(avatarName);
|
||||
@ -812,7 +813,7 @@ router.post('/edit', urlencodedParser, async function (request, response) {
|
||||
await writeCharacterData(avatarPath, char, targetFile, request);
|
||||
} else {
|
||||
const crop = tryParse(request.query.crop);
|
||||
const newAvatarPath = path.join(UPLOADS_PATH, request.file.filename);
|
||||
const newAvatarPath = path.join(request.file.destination, request.file.filename);
|
||||
invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
|
||||
await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
|
||||
fs.unlinkSync(newAvatarPath);
|
||||
@ -1096,7 +1097,7 @@ function getPreservedName(request) {
|
||||
router.post('/import', urlencodedParser, async function (request, response) {
|
||||
if (!request.body || !request.file) return response.sendStatus(400);
|
||||
|
||||
const uploadPath = path.join(UPLOADS_PATH, request.file.filename);
|
||||
const uploadPath = path.join(request.file.destination, request.file.filename);
|
||||
const format = request.body.file_type;
|
||||
const preservedFileName = getPreservedName(request);
|
||||
|
||||
|
@ -6,7 +6,6 @@ const sanitize = require('sanitize-filename');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { UPLOADS_PATH } = require('../constants');
|
||||
const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util');
|
||||
|
||||
/**
|
||||
@ -323,7 +322,7 @@ router.post('/group/import', urlencodedParser, function (request, response) {
|
||||
}
|
||||
|
||||
const chatname = humanizedISO8601DateTime();
|
||||
const pathToUpload = path.join(UPLOADS_PATH, filedata.filename);
|
||||
const pathToUpload = path.join(filedata.destination, filedata.filename);
|
||||
const pathToNewFile = path.join(request.user.directories.groupChats, `${chatname}.jsonl`);
|
||||
fs.copyFileSync(pathToUpload, pathToNewFile);
|
||||
fs.unlinkSync(pathToUpload);
|
||||
@ -347,9 +346,11 @@ router.post('/import', urlencodedParser, function (request, response) {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(path.join(UPLOADS_PATH, request.file.filename), 'utf8');
|
||||
const pathToUpload = path.join(request.file.destination, request.file.filename);
|
||||
const data = fs.readFileSync(pathToUpload, 'utf8');
|
||||
|
||||
if (format === 'json') {
|
||||
fs.unlinkSync(pathToUpload);
|
||||
const jsonData = JSON.parse(data);
|
||||
if (jsonData.histories !== undefined) {
|
||||
// CAI Tools format
|
||||
@ -388,7 +389,8 @@ router.post('/import', urlencodedParser, function (request, response) {
|
||||
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
|
||||
const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
|
||||
const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
|
||||
fs.copyFileSync(path.join(UPLOADS_PATH, request.file.filename), filePath);
|
||||
fs.copyFileSync(pathToUpload, filePath);
|
||||
fs.unlinkSync(pathToUpload);
|
||||
response.send({ res: true });
|
||||
} else {
|
||||
console.log('Incorrect chat format .jsonl');
|
||||
|
@ -5,7 +5,6 @@ const express = require('express');
|
||||
const mime = require('mime-types');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { UPLOADS_PATH } = require('../constants');
|
||||
const { getImageBuffers } = require('../util');
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
|
||||
@ -190,7 +189,7 @@ router.post('/upload-zip', urlencodedParser, async (request, response) => {
|
||||
return response.sendStatus(404);
|
||||
}
|
||||
|
||||
const spritePackPath = path.join(UPLOADS_PATH, file.filename);
|
||||
const spritePackPath = path.join(file.destination, file.filename);
|
||||
const sprites = await getImageBuffers(spritePackPath);
|
||||
const files = fs.readdirSync(spritesPath);
|
||||
|
||||
@ -248,7 +247,7 @@ router.post('/upload', urlencodedParser, async (request, response) => {
|
||||
}
|
||||
|
||||
const filename = label + path.parse(file.originalname).ext;
|
||||
const spritePath = path.join(UPLOADS_PATH, file.filename);
|
||||
const spritePath = path.join(file.destination, file.filename);
|
||||
const pathToFile = path.join(spritesPath, filename);
|
||||
// Copy uploaded file to sprites folder
|
||||
fs.cpSync(spritePath, pathToFile);
|
||||
|
@ -5,7 +5,6 @@ const sanitize = require('sanitize-filename');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { UPLOADS_PATH } = require('../constants');
|
||||
|
||||
/**
|
||||
* Reads a World Info file and returns its contents
|
||||
@ -74,7 +73,7 @@ router.post('/import', urlencodedParser, (request, response) => {
|
||||
if (request.body.convertedData) {
|
||||
fileContents = request.body.convertedData;
|
||||
} else {
|
||||
const pathToUpload = path.join(UPLOADS_PATH, request.file.filename);
|
||||
const pathToUpload = path.join(request.file.destination, request.file.filename);
|
||||
fileContents = fs.readFileSync(pathToUpload, 'utf8');
|
||||
fs.unlinkSync(pathToUpload);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user