diff --git a/default/content/presets/openai/Default.json b/default/content/presets/openai/Default.json index 6c3823086..e32590a32 100644 --- a/default/content/presets/openai/Default.json +++ b/default/content/presets/openai/Default.json @@ -1,7 +1,7 @@ { "chat_completion_source": "openai", - "openai_model": "gpt-3.5-turbo", - "claude_model": "claude-instant-v1", + "openai_model": "gpt-4-turbo", + "claude_model": "claude-3-5-sonnet-20240620", "windowai_model": "", "openrouter_model": "OR_Website", "openrouter_use_fallback": false, @@ -9,7 +9,7 @@ "openrouter_group_models": false, "openrouter_sort_models": "alphabetically", "ai21_model": "j2-ultra", - "mistralai_model": "mistral-medium-latest", + "mistralai_model": "mistral-large-latest", "custom_model": "", "custom_url": "", "custom_include_body": "", diff --git a/default/content/settings.json b/default/content/settings.json index 1a62513a3..1140accb0 100644 --- a/default/content/settings.json +++ b/default/content/settings.json @@ -610,9 +610,9 @@ } ] }, - "wi_format": "[Details of the fictional world the RP is set in:\n{0}]\n", - "openai_model": "gpt-3.5-turbo", - "claude_model": "claude-instant-v1", + "wi_format": "{0}", + "openai_model": "gpt-4-turbo", + "claude_model": "claude-3-5-sonnet-20240620", "ai21_model": "j2-ultra", "windowai_model": "", "openrouter_model": "OR_Website", diff --git a/public/img/step-into.svg b/public/img/step-into.svg new file mode 100644 index 000000000..fcfa7ef16 --- /dev/null +++ b/public/img/step-into.svg @@ -0,0 +1,149 @@ + + + + diff --git a/public/img/step-out.svg b/public/img/step-out.svg new file mode 100644 index 000000000..aa7dd3ea2 --- /dev/null +++ b/public/img/step-out.svg @@ -0,0 +1,149 @@ + + + + diff --git a/public/img/step-over.svg b/public/img/step-over.svg new file mode 100644 index 000000000..6f23ff22a --- /dev/null +++ b/public/img/step-over.svg @@ -0,0 +1,149 @@ + + + + diff --git a/public/img/step-resume.svg b/public/img/step-resume.svg new file mode 100644 index 000000000..bf3e0647f --- /dev/null +++ b/public/img/step-resume.svg @@ -0,0 +1,218 @@ + + + + diff --git a/public/index.html b/public/index.html index f59c28e7e..63d5a98ac 100644 --- a/public/index.html +++ b/public/index.html @@ -1273,7 +1273,7 @@ -
+

@@ -1358,7 +1358,7 @@

-
+

@@ -1387,7 +1387,7 @@

-
+

-
+

Contrastive Search

@@ -1544,7 +1544,7 @@

-
+

Samplers Order
- Samplers will be applied in a top-down order. + kcpp only. Samplers will be applied in a top-down order. Use with caution.
@@ -1597,10 +1597,10 @@ Load default order
-
+

- Samplers Order + Sampler Order

@@ -1618,7 +1618,7 @@ Load default order
-
+

Sampler Priority @@ -1770,13 +1770,13 @@

-
+
@@ -2802,21 +2802,26 @@

Google Model

@@ -2895,6 +2900,16 @@

Perplexity Model

- Use as Stop Strings + Separators as Stop Strings + + - +
+ + +
-
@@ -4144,6 +4163,12 @@ Quick "Continue" button +
+
+ + +
@@ -5811,6 +5846,7 @@ Enable simple UI mode +

Looking for AI characters? @@ -5829,6 +5865,7 @@

+

Your Persona

@@ -6342,6 +6379,7 @@
+
@@ -6520,4 +6558,4 @@ - + \ No newline at end of file diff --git a/public/locales/ar-sa.json b/public/locales/ar-sa.json index da45cca28..7758ce879 100644 --- a/public/locales/ar-sa.json +++ b/public/locales/ar-sa.json @@ -207,7 +207,7 @@ "JSON Schema": "مخطط جيسون", "Type in the desired JSON schema": "اكتب مخطط JSON المطلوب", "Grammar String": "سلسلة القواعد", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "يعتمد GNBF أو ENBF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "يعتمد GBNF أو EBNF على الواجهة الخلفية المستخدمة. إذا كنت تستخدم هذا يجب أن تعرف أي.", "Top P & Min P": "أعلى ع وأدنى ص", "Load default order": "تحميل الترتيب الافتراضي", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp فقط. تحديد ترتيب أخذ العينات. إذا لم يكن وضع Mirostat 0، فسيتم تجاهل ترتيب أخذ العينات.", diff --git a/public/locales/de-de.json b/public/locales/de-de.json index 2ef78556d..95e496f44 100644 --- a/public/locales/de-de.json +++ b/public/locales/de-de.json @@ -207,7 +207,7 @@ "JSON Schema": "JSON-Schema", "Type in the desired JSON schema": "Geben Sie das gewünschte JSON-Schema ein", "Grammar String": "Grammatikzeichenfolge", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF oder ENBF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF oder EBNF, hängt vom verwendeten Backend ab. Wenn Sie dieses verwenden, sollten Sie wissen, welches.", "Top P & Min P": "Top P und Min P", "Load default order": "Standardreihenfolge laden", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Nur llama.cpp. Bestimmt die Reihenfolge der Sampler. Wenn der Mirostat-Modus nicht 0 ist, wird die Sampler-Reihenfolge ignoriert.", diff --git a/public/locales/es-es.json b/public/locales/es-es.json index be2735ca8..126c8efea 100644 --- a/public/locales/es-es.json +++ b/public/locales/es-es.json @@ -207,7 +207,7 @@ "JSON Schema": "Esquema JSON", "Type in the desired JSON schema": "Escriba el esquema JSON deseado", "Grammar String": "Cadena de gramática", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF o ENBF, depende del backend en uso. Si estás usando esto, debes saber cuál.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF o EBNF, depende del backend en uso. Si estás usando esto, debes saber cuál.", "Top P & Min P": "P superior y P mínima", "Load default order": "Cargar orden predeterminado", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp únicamente. Determina el orden de los muestreadores. Si el modo Mirostat no es 0, se ignora el orden de las muestras.", diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index ad4e61d1c..f24eaa295 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -207,7 +207,7 @@ "JSON Schema": "Schéma JSON", "Type in the desired JSON schema": "Tapez le schéma JSON souhaité", "Grammar String": "Chaîne de grammaire", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF ou ENBF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF ou EBNF dépend du backend utilisé. Si vous l'utilisez, vous devez savoir lequel.", "Top P & Min P": "P supérieur et P minimal", "Load default order": "Charger l'ordre par défaut", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "lama.cpp uniquement. Détermine l’ordre des échantillonneurs. Si le mode Mirostat n'est pas 0, l'ordre de l'échantillonneur est ignoré.", diff --git a/public/locales/is-is.json b/public/locales/is-is.json index 31db6ec55..41d9a966d 100644 --- a/public/locales/is-is.json +++ b/public/locales/is-is.json @@ -207,7 +207,7 @@ "JSON Schema": "JSON kerfi", "Type in the desired JSON schema": "Sláðu inn æskilegt JSON skema", "Grammar String": "Málfræðistrengur", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF eða ENBF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF eða EBNF, fer eftir bakendanum sem er í notkun. Ef þú ert að nota þetta ættir þú að vita hvaða.", "Top P & Min P": "Efstu P & Min P", "Load default order": "Hlaða sjálfgefna röð", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp eingöngu. Ákveður röð sýnataka. Ef Mirostat hamur er ekki 0, er röð sýnatöku hunsuð.", diff --git a/public/locales/it-it.json b/public/locales/it-it.json index ab581c550..bad81f01e 100644 --- a/public/locales/it-it.json +++ b/public/locales/it-it.json @@ -207,7 +207,7 @@ "JSON Schema": "Schema JSON", "Type in the desired JSON schema": "Digita lo schema JSON desiderato", "Grammar String": "Stringa grammaticale", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF o ENBF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF o EBNF, dipende dal backend in uso. Se stai usando questo dovresti sapere quale.", "Top P & Min P": "P massimo e P minimo", "Load default order": "Carica ordine predefinito", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "Solo lama.cpp. Determina l'ordine dei campionatori. Se la modalità Mirostat non è 0, l'ordine del campionatore viene ignorato.", diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index 3c27994c3..51e4da5c8 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -207,7 +207,7 @@ "JSON Schema": "JSONスキーマ", "Type in the desired JSON schema": "希望するJSONスキーマを入力します", "Grammar String": "文法文字列", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF または ENBF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF または EBNF は、使用するバックエンドによって異なります。これを使用する場合は、どちらであるかを知っておく必要があります。", "Top P & Min P": "トップPと最小P", "Load default order": "デフォルトの順序を読み込む", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp のみ。サンプラーの順序を決定します。Mirostat モードが 0 でない場合、サンプラーの順序は無視されます。", diff --git a/public/locales/ko-kr.json b/public/locales/ko-kr.json index 05c252703..2ead98ace 100644 --- a/public/locales/ko-kr.json +++ b/public/locales/ko-kr.json @@ -207,7 +207,7 @@ "JSON Schema": "JSON 스키마", "Type in the desired JSON schema": "원하는 JSON 스키마를 입력하세요.", "Grammar String": "문법 문자열", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 또는 ENBF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 또는 EBNF는 사용 중인 백엔드에 따라 다릅니다. 이것을 사용한다면 어느 것이 무엇인지 알아야합니다.", "Top P & Min P": "상위 P 및 최소 P", "Load default order": "기본 순서로 로드", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp만 가능합니다. 샘플러의 순서를 결정합니다. Mirostat 모드가 0이 아닌 경우 샘플러 순서는 무시됩니다.", diff --git a/public/locales/nl-nl.json b/public/locales/nl-nl.json index dd4c9da96..2320db081 100644 --- a/public/locales/nl-nl.json +++ b/public/locales/nl-nl.json @@ -207,7 +207,7 @@ "JSON Schema": "JSON-schema", "Type in the desired JSON schema": "Typ het gewenste JSON-schema", "Grammar String": "Grammaticareeks", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF of ENBF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF of EBNF, hangt af van de gebruikte backend. Als u dit gebruikt, moet u weten welke.", "Top P & Min P": "Top P & Min P", "Load default order": "Standaardvolgorde laden", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "alleen lama.cpp. Bepaalt de volgorde van de samplers. Als de Mirostat-modus niet 0 is, wordt de samplervolgorde genegeerd.", diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json index 8f4fe5cd3..d671fea47 100644 --- a/public/locales/pt-pt.json +++ b/public/locales/pt-pt.json @@ -207,7 +207,7 @@ "JSON Schema": "Esquema JSON", "Type in the desired JSON schema": "Digite o esquema JSON desejado", "Grammar String": "Cadeia de Gramática", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF ou ENBF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF ou EBNF, depende do backend em uso. Se você estiver usando isso, você deve saber qual.", "Top P & Min P": "P superior e P mínimo", "Load default order": "Carregar ordem padrão", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "apenas lhama.cpp. Determina a ordem dos amostradores. Se o modo Mirostat não for 0, a ordem do amostrador será ignorada.", diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json index 80dc39041..101315211 100644 --- a/public/locales/ru-ru.json +++ b/public/locales/ru-ru.json @@ -978,7 +978,7 @@ "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в", "char_import_7": ")", "Grammar String": "Грамматика", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF или ENBF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", "Account": "Аккаунт", "Hi,": "Привет,", "To enable multi-account features, restart the SillyTavern server with": "Чтобы активировать систему аккаунтов, перезапустите SillyTavern, выставив", diff --git a/public/locales/uk-ua.json b/public/locales/uk-ua.json index cb164b5b2..12290f996 100644 --- a/public/locales/uk-ua.json +++ b/public/locales/uk-ua.json @@ -207,7 +207,7 @@ "JSON Schema": "Схема JSON", "Type in the desired JSON schema": "Введіть потрібну схему JSON", "Grammar String": "Граматичний рядок", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF або ENBF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF або EBNF, залежить від серверної частини, яка використовується. Якщо ви використовуєте це, ви повинні знати, який.", "Top P & Min P": "Верхній P & Min P", "Load default order": "Завантажити типовий порядок", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "лише llama.cpp. Визначає порядок пробовідбірників. Якщо режим Mirostat не 0, порядок вибірки ігнорується.", diff --git a/public/locales/vi-vn.json b/public/locales/vi-vn.json index 547423bbb..c90edf84f 100644 --- a/public/locales/vi-vn.json +++ b/public/locales/vi-vn.json @@ -207,7 +207,7 @@ "JSON Schema": "Lược đồ JSON", "Type in the desired JSON schema": "Nhập lược đồ JSON mong muốn", "Grammar String": "Chuỗi ngữ pháp", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF hoặc ENBF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF hoặc EBNF, tùy thuộc vào backend đang sử dụng. Nếu bạn đang sử dụng cái này, bạn nên biết cái nào.", "Top P & Min P": "P & P tối thiểu hàng đầu", "Load default order": "Tải thứ tự mặc định", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "chỉ llama.cpp. Xác định thứ tự lấy mẫu. Nếu chế độ Mirostat khác 0, thứ tự lấy mẫu sẽ bị bỏ qua.", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 2c2271217..0519388e7 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -208,9 +208,10 @@ "JSON Schema": "JSON 结构", "Type in the desired JSON schema": "输入所需的 JSON 结构", "Grammar String": "语法字符串", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 或 ENBF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取决于使用的后端。如果您使用这个,您应该知道该用哪一个。", "Top P & Min P": "Top P 和 Min P", "Load default order": "加载默认顺序", + "Sampler Order": "取样器顺序", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "仅限 llama.cpp。确定采样器的顺序。如果 Mirostat 模式不为 0,则忽略采样器顺序。", "Sampler Priority": "采样器优先级", "Ooba only. Determines the order of samplers.": "确定采样器的顺序(仅适用于Ooba)", @@ -443,7 +444,9 @@ "Example Separator": "示例分隔符", "Chat Start": "聊天开始", "Add Chat Start and Example Separator to a list of stopping strings.": "将聊天开始和示例分隔符添加到停止字符串列表中。", - "Use as Stop Strings": "用作停止字符串", + "Separators as Stop Strings": "分隔符作为终止字符串", + "Add Character and User names to a list of stopping strings.": "将角色和用户名添加到停止字符串列表中。", + "Names as Stop Strings": "名称作为终止字符串", "context_allow_post_history_instructions": "如果在角色卡中定义并且启用了“首选角色卡说明”,则在提示末尾包含后历史说明。\n不建议在文本补全模型中使用此功能,否则会导致输出错误。", "Allow Post-History Instructions": "允许后历史说明", "Context Order": "上下文顺序", @@ -772,6 +775,10 @@ "Autocomplete Style": "风格", "Follow Theme": "关注主题", "Dark": "黑暗的", + "Keyboard": "键盘:", + "Select with Tab or Enter": "使用 Tab 或 Enter 选择", + "Select with Tab": "使用 Tab 选择", + "Select with Enter": "按 Enter 键选择", "Sets the font size of the autocomplete.": "设置自动完成的字体大小。", "Sets the width of the autocomplete.": "设置自动完成的宽度。", "Autocomplete Width": "宽度", @@ -1341,6 +1348,7 @@ "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。", "Labels and Message": "标签和信息", "Label": "标签", + "(label of the button, if no icon is chosen) ": "(如果没有选择图标,则为按钮的标签)", "Title": "标题", "(tooltip, leave empty to show message or /command)": "(工具提示,留空以显示消息或/命令)", "Message / Command:": "消息/命令:", @@ -1371,6 +1379,8 @@ "Inject user input automatically": "自动注入用户输入", "(if disabled, use ": "(如果禁用,使用", "macro for manual injection)": "宏用于手动注入)", + "Color": "颜色", + "Only apply color as accent": "仅应用颜色作为强调", "ext_regex_title": "正则", "ext_regex_new_global_script_desc": "新的全局正则表达式脚本", "ext_regex_new_global_script": "新建全局正则", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index c188f2f1f..5744e5b93 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -208,7 +208,7 @@ "JSON Schema": "JSON 結構", "Type in the desired JSON schema": "輸入所需的 JSON 結構", "Grammar String": "語法字串", - "GNBF or ENBF, depends on the backend in use. If you're using this you should know which.": "GNBF 或 ENBF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種", + "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF 或 EBNF,取決於所使用的後端。如果您使用此功能,應該知道是哪一種", "Top P & Min P": "Top P 和 Min P", "Load default order": "載入預設順序", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "僅適用於 llama.cpp。決定取樣器的順序。如果 Mirostat 模式不為 0,則忽略取樣器順序。", diff --git a/public/script.js b/public/script.js index 6cddb1ae6..22745982e 100644 --- a/public/script.js +++ b/public/script.js @@ -228,7 +228,7 @@ import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay import { loadFeatherlessModels, 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 { MacrosParser, evaluateMacros } from './scripts/macros.js'; +import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; @@ -440,6 +440,7 @@ export const event_types = { GROUP_CHAT_CREATED: 'group_chat_created', GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts', GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts', + GENERATE_AFTER_DATA: 'generate_after_data', GROUP_MEMBER_DRAFTED: 'group_member_drafted', WORLD_INFO_ACTIVATED: 'world_info_activated', TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready', @@ -1721,16 +1722,24 @@ export async function replaceCurrentChat() { } export function showMoreMessages() { - let messageId = Number($('#chat').children('.mes').first().attr('mesid')); + const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); + let messageId = Number(firstDisplayedMesId); let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; + // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, + // so the first "new" message being shown will be the last available message + if (isNaN(messageId)) { + messageId = getLastMessageId() + 1; + } + console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); const prevHeight = $('#chat').prop('scrollHeight'); while (messageId > 0 && count > 0) { + let newMessageId = messageId - 1; + addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId }); count--; messageId--; - addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId }); } if (messageId == 0) { @@ -2471,26 +2480,30 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re * @returns {string[]} Array of stopping strings */ export function getStoppingStrings(isImpersonate, isContinue) { - const charString = `\n${name2}:`; - const userString = `\n${name1}:`; - const result = isImpersonate ? [charString] : [userString]; + const result = []; - result.push(userString); + if (power_user.context.names_as_stop_strings) { + const charString = `\n${name2}:`; + const userString = `\n${name1}:`; + result.push(isImpersonate ? charString : userString); - if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) { - result.push(charString); - } + result.push(userString); - // Add other group members as the stopping strings - if (selected_group) { - const group = groups.find(x => x.id === selected_group); + if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) { + result.push(charString); + } - if (group && Array.isArray(group.members)) { - const names = group.members - .map(x => characters.find(y => y.avatar == x)) - .filter(x => x && x.name && x.name !== name2) - .map(x => `\n${x.name}:`); - result.push(...names); + // Add other group members as the stopping strings + if (selected_group) { + const group = groups.find(x => x.id === selected_group); + + if (group && Array.isArray(group.members)) { + const names = group.members + .map(x => characters.find(y => y.avatar == x)) + .filter(x => x && x.name && x.name !== name2) + .map(x => `\n${x.name}:`); + result.push(...names); + } } } @@ -4212,6 +4225,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro } } + await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data); + if (dryRun) { generatedPromptCache = ''; return Promise.resolve(); @@ -5075,7 +5090,7 @@ function setInContextMessages(lastmsg, type) { * @param {object} data Generation data * @returns {Promise} Response data from the API */ -async function sendGenerationRequest(type, data) { +export async function sendGenerationRequest(type, data) { if (main_api === 'openai') { return await sendOpenAIRequest(type, data.prompt, abortController.signal); } @@ -5107,7 +5122,7 @@ async function sendGenerationRequest(type, data) { * @param {object} data Generation data * @returns {Promise} Streaming generator */ -async function sendStreamingRequest(type, data) { +export async function sendStreamingRequest(type, data) { if (abortController?.signal?.aborted) { throw new Error('Generation was aborted.'); } @@ -5611,6 +5626,7 @@ export function activateSendButtons() { is_send_press = false; $('#send_but').removeClass('displayNone'); $('#mes_continue').removeClass('displayNone'); + $('#mes_impersonate').removeClass('displayNone'); $('.mes_buttons:last').show(); hideStopButton(); } @@ -5618,6 +5634,7 @@ export function activateSendButtons() { export function deactivateSendButtons() { $('#send_but').addClass('displayNone'); $('#mes_continue').addClass('displayNone'); + $('#mes_impersonate').addClass('displayNone'); showStopButton(); } @@ -6408,7 +6425,7 @@ export async function getSettings() { loadHordeSettings(settings); // Load power user settings - loadPowerUserSettings(settings, data); + await loadPowerUserSettings(settings, data); // Load character tags loadTagsSettings(settings); @@ -7918,6 +7935,8 @@ window['SillyTavern'].getContext = function () { eventTypes: event_types, addOneMessage: addOneMessage, generate: Generate, + sendStreamingRequest: sendStreamingRequest, + sendGenerationRequest: sendGenerationRequest, stopGeneration: stopGeneration, getTokenCount: getTokenCount, extensionPrompts: extension_prompts, @@ -8497,7 +8516,7 @@ export async function processDroppedFiles(files, data = new Map()) { for (const file of files) { const extension = file.name.split('.').pop().toLowerCase(); - if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) { + if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { const preservedName = data instanceof Map && data.get(file); await importCharacter(file, preservedName); } else { @@ -9096,14 +9115,14 @@ jQuery(async function () { $('#send_textarea').on('focusin focus click', () => { S_TAPreviouslyFocused = true; }); - $('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => { + $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => { if (S_TAPreviouslyFocused) { $('#send_textarea').focus(); } }); $(document).click(event => { if ($(':focus').attr('id') !== 'send_textarea') { - var validIDs = ['options_button', 'send_but', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue']; + var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue']; if (!validIDs.includes($(event.target).attr('id'))) { S_TAPreviouslyFocused = false; } @@ -9139,6 +9158,9 @@ jQuery(async function () { debouncedCharacterSearch(searchQuery); }); + $('#mes_impersonate').on('click', function () { + $('#option_impersonate').trigger('click'); + }); $('#mes_continue').on('click', function () { $('#option_continue').trigger('click'); @@ -10436,8 +10458,9 @@ jQuery(async function () { } // Set the height of "autoSetHeight" textareas within the drawer to their scroll height - $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(function () { - resetScrollHeight($(this)); + $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () { + await resetScrollHeight($(this)); + return; }); } else if (drawerWasOpenAlready) { //to close manually @@ -10510,8 +10533,9 @@ jQuery(async function () { $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle(); // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height - $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(function () { - resetScrollHeight($(this)); + $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(async function () { + await resetScrollHeight($(this)); + return; }); }); diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 2cfeb1f6b..721d2bd53 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -311,6 +311,7 @@ function RA_checkOnlineStatus() { $('#send_form').addClass('no-connection'); //entire input form area is red when not connected $('#send_but').addClass('displayNone'); //send button is hidden when not connected; $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected; + $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected; $('#API-status-top').removeClass('fa-plug'); $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow'); connection_made = false; @@ -327,6 +328,7 @@ function RA_checkOnlineStatus() { if (!is_send_press && !(selected_group && is_group_generating)) { $('#send_but').removeClass('displayNone'); //on connect, send button shows $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected + $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected } } } diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index df3bcf75c..ba3d427a2 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = { 'FULL': 2, }; +/**@readonly*/ +/**@enum {Number}*/ +export const AUTOCOMPLETE_SELECT_KEY = { + 'TAB': 1, // 2^0 + 'ENTER': 2, // 2^1 +}; + export class AutoComplete { - /**@type {HTMLTextAreaElement}*/ textarea; + /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea; /**@type {boolean}*/ isFloating = false; /**@type {()=>boolean}*/ checkIfActivate; /**@type {(text:string, index:number) => Promise}*/ getNameAt; @@ -56,6 +63,8 @@ export class AutoComplete { /**@type {function}*/ updateDetailsPositionDebounced; /**@type {function}*/ updateFloatingPositionDebounced; + /**@type {(item:AutoCompleteOption)=>any}*/ onSelect; + get matchType() { return power_user.stscript.matching ?? 'fuzzy'; } @@ -68,7 +77,7 @@ export class AutoComplete { /** - * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete. + * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete. * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/') * @param {(text: string, index: number) => Promise} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text. * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor. @@ -102,10 +111,15 @@ export class AutoComplete { this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10); this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10); - textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced)); + textarea.addEventListener('input', ()=>{ + this.selectionStart = this.textarea.selectionStart; + if (this.text != this.textarea.value) this.show(true, this.wasForced); + }); textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt)); - textarea.addEventListener('click', ()=>this.isActive ? this.show() : null); - textarea.addEventListener('selectionchange', ()=>this.show()); + textarea.addEventListener('click', ()=>{ + this.selectionStart = this.textarea.selectionStart; + if (this.isActive) this.show(); + }); textarea.addEventListener('blur', ()=>this.hide()); if (isFloating) { textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced()); @@ -189,6 +203,11 @@ export class AutoComplete { * @returns The option. */ fuzzyScore(option) { + // might have been matched by the options matchProvider function instead + if (!this.fuzzyRegex.test(option.name)) { + option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1); + return option; + } const parts = this.fuzzyRegex.exec(option.name).slice(1, -1); let start = null; let consecutive = []; @@ -339,7 +358,7 @@ export class AutoComplete { this.result = this.effectiveParserResult.optionList // filter the list of options by the partial name according to the matching type - .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name) + .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name) // remove aliases .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx); @@ -357,10 +376,11 @@ export class AutoComplete { // build element option.dom = this.makeItem(option); // update replacer and add quotes if necessary + const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name; if (this.effectiveParserResult.canBeQuoted) { - option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`; + option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName.replace(/"/g, '\\"')}"` : `${optionName}`; } else { - option.replacer = option.name; + option.replacer = optionName; } // calculate fuzzy score if matching is fuzzy if (this.matchType == 'fuzzy') this.fuzzyScore(option); @@ -399,7 +419,7 @@ export class AutoComplete { , ); this.result.push(option); - } else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) { + } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) { // only one result that is exactly the current value? just show hint, no autocomplete this.isReplaceable = false; this.isShowingDetails = false; @@ -439,11 +459,14 @@ export class AutoComplete { } else { item.dom.classList.remove('selected'); } + if (!item.isSelectable) { + item.dom.classList.add('not-selectable'); + } frag.append(item.dom); } this.dom.append(frag); this.updatePosition(); - getTopmostModalLayer().append(this.domWrap); + this.getLayer().append(this.domWrap); } else { this.domWrap.remove(); } @@ -458,10 +481,17 @@ export class AutoComplete { if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove(); this.detailsDom.innerHTML = ''; this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM'); - getTopmostModalLayer().append(this.detailsWrap); + this.getLayer().append(this.detailsWrap); this.updateDetailsPositionDebounced(); } + /** + * @returns {HTMLElement} closest ancestor dialog or body + */ + getLayer() { + return this.textarea.closest('dialog, body'); + } + /** @@ -474,7 +504,7 @@ export class AutoComplete { const rect = {}; rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); - rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect(); + rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect(); this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`; @@ -501,7 +531,7 @@ export class AutoComplete { const rect = {}; rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); - rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect(); + rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect(); if (this.isReplaceable) { this.detailsWrap.classList.remove('full'); const selRect = this.selectedItem.dom.children[0].getBoundingClientRect(); @@ -527,32 +557,34 @@ export class AutoComplete { updateFloatingPosition() { const location = this.getCursorPosition(); const rect = this.textarea.getBoundingClientRect(); + const layerRect = this.getLayer().getBoundingClientRect(); // cursor is out of view -> hide if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { return this.hide(); } - const left = Math.max(rect.left, location.left); + const left = Math.max(rect.left, location.left) - layerRect.left; this.domWrap.style.setProperty('--targetOffset', `${left}`); if (location.top <= window.innerHeight / 2) { // if cursor is in lower half of window, show list above line - this.domWrap.style.top = `${location.bottom}px`; + this.domWrap.style.top = `${location.bottom - layerRect.top}px`; this.domWrap.style.bottom = 'auto'; - this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`; + this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } else { // if cursor is in upper half of window, show list below line this.domWrap.style.top = 'auto'; - this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`; - this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`; + this.domWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`; + this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } } updateFloatingDetailsPosition(location = null) { if (!location) location = this.getCursorPosition(); const rect = this.textarea.getBoundingClientRect(); + const layerRect = this.getLayer().getBoundingClientRect(); if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { return this.hide(); } - const left = Math.max(rect.left, location.left); + const left = Math.max(rect.left, location.left) - layerRect.left; this.detailsWrap.style.setProperty('--targetOffset', `${left}`); if (this.isReplaceable) { this.detailsWrap.classList.remove('full'); @@ -572,14 +604,14 @@ export class AutoComplete { } if (location.top <= window.innerHeight / 2) { // if cursor is in lower half of window, show list above line - this.detailsWrap.style.top = `${location.bottom}px`; + this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`; this.detailsWrap.style.bottom = 'auto'; - this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`; + this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } else { // if cursor is in upper half of window, show list below line this.detailsWrap.style.top = 'auto'; - this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`; - this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`; + this.detailsWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`; + this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } } @@ -597,7 +629,7 @@ export class AutoComplete { } this.clone.style.position = 'fixed'; this.clone.style.visibility = 'hidden'; - getTopmostModalLayer().append(this.clone); + document.body.append(this.clone); const mo = new MutationObserver(muts=>{ if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) { this.clone.remove(); @@ -656,6 +688,7 @@ export class AutoComplete { } this.wasForced = false; this.textarea.dispatchEvent(new Event('input', { bubbles:true })); + this.onSelect?.(this.selectedItem); } @@ -708,8 +741,10 @@ export class AutoComplete { } case 'Enter': { // pick the selected item to autocomplete + if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break; if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; if (this.selectedItem.name == this.name) break; + if (!this.selectedItem.isSelectable) break; evt.preventDefault(); evt.stopImmediatePropagation(); this.select(); @@ -717,9 +752,11 @@ export class AutoComplete { } case 'Tab': { // pick the selected item to autocomplete + if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break; if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; evt.preventDefault(); evt.stopImmediatePropagation(); + if (!this.selectedItem.isSelectable) break; this.select(); return; } @@ -772,30 +809,16 @@ export class AutoComplete { // ignore keydown on modifier keys return; } - switch (evt.key) { - case 'ArrowUp': - case 'ArrowDown': - case 'ArrowRight': - case 'ArrowLeft': { - if (this.isActive) { - // keyboard navigation, wait for keyup to complete cursor move - const oldText = this.textarea.value; - await new Promise(resolve=>{ - window.addEventListener('keyup', resolve, { once:true }); - }); - if (this.selectionStart != this.textarea.selectionStart) { - this.selectionStart = this.textarea.selectionStart; - this.show(this.isReplaceable || oldText != this.textarea.value); - } - } - break; - } - default: { - if (this.isActive) { - this.text != this.textarea.value && this.show(this.isReplaceable); - } - break; - } + // await keyup to see if cursor position or text has changed + const oldText = this.textarea.value; + await new Promise(resolve=>{ + window.addEventListener('keyup', resolve, { once:true }); + }); + if (this.selectionStart != this.textarea.selectionStart) { + this.selectionStart = this.textarea.selectionStart; + this.show(this.isReplaceable || oldText != this.textarea.value); + } else if (this.isActive) { + this.text != this.textarea.value && this.show(this.isReplaceable); } } } diff --git a/public/scripts/autocomplete/AutoCompleteNameResult.js b/public/scripts/autocomplete/AutoCompleteNameResult.js index 41c19cf9f..f048d6383 100644 --- a/public/scripts/autocomplete/AutoCompleteNameResult.js +++ b/public/scripts/autocomplete/AutoCompleteNameResult.js @@ -1,36 +1,9 @@ -import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js'; -import { AutoCompleteOption } from './AutoCompleteOption.js'; -// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; +import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js'; +import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; -export class AutoCompleteNameResult { - /**@type {string} */ name; - /**@type {number} */ start; - /**@type {AutoCompleteOption[]} */ optionList = []; - /**@type {boolean} */ canBeQuoted = false; - /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; - /**@type {()=>string} */ makeNoOptionsText = ()=>'No options'; - - - /** - * @param {string} name Name (potentially partial) of the name at the requested index. - * @param {number} start Index where the name starts. - * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. - * @param {boolean} canBeQuoted Whether the name can be inside quotes. - * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. - * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. - */ - constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { - this.name = name; - this.start = start; - this.optionList = optionList; - this.canBeQuoted = canBeQuoted; - this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; - this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText; - } - - +export class AutoCompleteNameResult extends AutoCompleteNameResultBase { /** * * @param {string} text The whole text diff --git a/public/scripts/autocomplete/AutoCompleteNameResultBase.js b/public/scripts/autocomplete/AutoCompleteNameResultBase.js new file mode 100644 index 000000000..150ee68c5 --- /dev/null +++ b/public/scripts/autocomplete/AutoCompleteNameResultBase.js @@ -0,0 +1,31 @@ +import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js'; +import { AutoCompleteOption } from './AutoCompleteOption.js'; + + + +export class AutoCompleteNameResultBase { + /**@type {string} */ name; + /**@type {number} */ start; + /**@type {AutoCompleteOption[]} */ optionList = []; + /**@type {boolean} */ canBeQuoted = false; + /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; + /**@type {()=>string} */ makeNoOptionsText = ()=>'No options'; + + + /** + * @param {string} name Name (potentially partial) of the name at the requested index. + * @param {number} start Index where the name starts. + * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. + * @param {boolean} canBeQuoted Whether the name can be inside quotes. + * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. + * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. + */ + constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { + this.name = name; + this.start = start; + this.optionList = optionList; + this.canBeQuoted = canBeQuoted; + this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; + this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText; + } +} diff --git a/public/scripts/autocomplete/AutoCompleteOption.js b/public/scripts/autocomplete/AutoCompleteOption.js index abfcf3ff6..24822750b 100644 --- a/public/scripts/autocomplete/AutoCompleteOption.js +++ b/public/scripts/autocomplete/AutoCompleteOption.js @@ -11,6 +11,9 @@ export class AutoCompleteOption { /**@type {AutoCompleteFuzzyScore}*/ score; /**@type {string}*/ replacer; /**@type {HTMLElement}*/ dom; + /**@type {(input:string)=>boolean}*/ matchProvider; + /**@type {(input:string)=>string}*/ valueProvider; + /**@type {boolean}*/ makeSelectable = false; /** @@ -21,14 +24,21 @@ export class AutoCompleteOption { return this.name; } + get isSelectable() { + return this.makeSelectable || !this.valueProvider; + } + /** * @param {string} name */ - constructor(name, typeIcon = ' ', type = '') { + constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) { this.name = name; this.typeIcon = typeIcon; this.type = type; + this.matchProvider = matchProvider; + this.valueProvider = valueProvider; + this.makeSelectable = makeSelectable; } diff --git a/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js b/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js index 63eccf99f..e0e65fc7c 100644 --- a/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js +++ b/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js @@ -1,6 +1,6 @@ -import { AutoCompleteNameResult } from './AutoCompleteNameResult.js'; +import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js'; -export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult { +export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultBase { /**@type {boolean}*/ isRequired = false; /**@type {boolean}*/ forceMatch = true; } diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index dbfbc0d1d..7194abc49 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -8,13 +8,12 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; -import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; export { MODULE_NAME }; const MODULE_NAME = 'caption'; -const PROMPT_DEFAULT = 'What’s in this image?'; +const PROMPT_DEFAULT = 'What\'s in this image?'; const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]'; /** @@ -170,7 +169,11 @@ async function sendCaptionedMessage(caption, image) { }, }; context.chat.push(message); + const messageId = context.chat.length - 1; + await eventSource.emit(event_types.MESSAGE_SENT, messageId); context.addOneMessage(message); + await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId); + await context.saveChat(); } /** @@ -334,7 +337,7 @@ async function getCaptionForFile(file, prompt, quiet) { } catch (error) { const errorMessage = error.message || 'Unknown error'; - toastr.error(errorMessage, "Failed to caption image."); + toastr.error(errorMessage, 'Failed to caption image.'); console.error(error); return ''; } @@ -399,6 +402,7 @@ jQuery(async function () { (modules.includes('caption') && extension_settings.caption.source === 'extras') || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) || + (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) || diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index 5181e8ce1..158e5f6d4 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -17,6 +17,7 @@
+ @@ -42,6 +44,8 @@ + + diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index efcfbdc7c..ab02d1a51 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1,4 +1,4 @@ -import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js'; +import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js'; import { dragElement, isMobile } from '../../RossAscends-mods.js'; import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; import { loadMovingUIState, power_user } from '../../power-user.js'; @@ -1156,7 +1156,7 @@ async function getExpressionLabel(text) { functionResult = args?.arguments; }); - const emotionResponse = await generateQuietPrompt(prompt, false, false); + const emotionResponse = await generateRaw(text, main_api, false, false, prompt); return parseLlmResponse(functionResult || emotionResponse, expressionsList); } // Extras diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 15ff1d4da..fedb6d0e6 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -23,10 +23,18 @@ export class QuickReplyApi { + /** + * @param {QuickReply} qr + * @returns {QuickReplySet} + */ + getSetByQr(qr) { + return QuickReplySet.list.find(it=>it.qrList.includes(qr)); + } + /** * Finds and returns an existing Quick Reply Set by its name. * - * @param {String} name name of the quick reply set + * @param {string} name name of the quick reply set * @returns the quick reply set, or undefined if not found */ getSetByName(name) { @@ -36,13 +44,14 @@ export class QuickReplyApi { /** * Finds and returns an existing Quick Reply by its set's name and its label. * - * @param {String} setName name of the quick reply set - * @param {String} label label of the quick reply + * @param {string} setName name of the quick reply set + * @param {string|number} label label or numeric ID of the quick reply * @returns the quick reply, or undefined if not found */ getQrByLabel(setName, label) { const set = this.getSetByName(setName); if (!set) return; + if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label); return set.qrList.find(it=>it.label == label); } @@ -70,24 +79,25 @@ export class QuickReplyApi { /** * Executes an existing quick reply. * - * @param {String} setName name of the existing quick reply set - * @param {String} label label of the existing quick reply (text on the button) - * @param {Object} [args] optional arguments + * @param {string} setName name of the existing quick reply set + * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID + * @param {object} [args] optional arguments + * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options */ - async executeQuickReply(setName, label, args = {}) { + async executeQuickReply(setName, label, args = {}, options = {}) { const qr = this.getQrByLabel(setName, label); if (!qr) { throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); } - return await qr.execute(args); + return await qr.execute(args, false, false, options); } /** * Adds or removes a quick reply set to the list of globally active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ toggleGlobalSet(name, isVisible = true) { const set = this.getSetByName(name); @@ -104,8 +114,8 @@ export class QuickReplyApi { /** * Adds a quick reply set to the list of globally active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ addGlobalSet(name, isVisible = true) { const set = this.getSetByName(name); @@ -118,7 +128,7 @@ export class QuickReplyApi { /** * Removes a quick reply set from the list of globally active quick reply sets. * - * @param {String} name the name of the set + * @param {string} name the name of the set */ removeGlobalSet(name) { const set = this.getSetByName(name); @@ -132,8 +142,8 @@ export class QuickReplyApi { /** * Adds or removes a quick reply set to the list of the current chat's active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ toggleChatSet(name, isVisible = true) { if (!this.settings.chatConfig) return; @@ -151,8 +161,8 @@ export class QuickReplyApi { /** * Adds a quick reply set to the list of the current chat's active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ addChatSet(name, isVisible = true) { if (!this.settings.chatConfig) return; @@ -166,7 +176,7 @@ export class QuickReplyApi { /** * Removes a quick reply set from the list of the current chat's active quick reply sets. * - * @param {String} name the name of the set + * @param {string} name the name of the set */ removeChatSet(name) { if (!this.settings.chatConfig) return; @@ -181,21 +191,25 @@ export class QuickReplyApi { /** * Creates a new quick reply in an existing quick reply set. * - * @param {String} setName name of the quick reply set to insert the new quick reply into - * @param {String} label label for the new quick reply (text on the button) - * @param {Object} [props] - * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply - * @param {String} [props.title] the title / tooltip to be shown on the quick reply button - * @param {Boolean} [props.isHidden] whether to hide or show the button - * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts - * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message - * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message - * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded - * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected - * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated + * @param {string} setName name of the quick reply set to insert the new quick reply into + * @param {string} label label for the new quick reply (text on the button) + * @param {object} [props] + * @param {string} [props.icon] the icon to show on the QR button + * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned + * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply + * @param {string} [props.title] the title / tooltip to be shown on the quick reply button + * @param {boolean} [props.isHidden] whether to hide or show the button + * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected + * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated * @returns {QuickReply} the new quick reply */ createQuickReply(setName, label, { + icon, + showLabel, message, title, isHidden, @@ -212,6 +226,8 @@ export class QuickReplyApi { } const qr = set.addQuickReply(); qr.label = label ?? ''; + qr.icon = icon ?? ''; + qr.showLabel = showLabel ?? false; qr.message = message ?? ''; qr.title = title ?? ''; qr.isHidden = isHidden ?? false; @@ -228,22 +244,26 @@ export class QuickReplyApi { /** * Updates an existing quick reply. * - * @param {String} setName name of the existing quick reply set - * @param {String} label label of the existing quick reply (text on the button) - * @param {Object} [props] - * @param {String} [props.newLabel] new label for quick reply (text on the button) - * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply - * @param {String} [props.title] the title / tooltip to be shown on the quick reply button - * @param {Boolean} [props.isHidden] whether to hide or show the button - * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts - * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message - * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message - * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded - * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected - * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated + * @param {string} setName name of the existing quick reply set + * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID + * @param {object} [props] + * @param {string} [props.icon] the icon to show on the QR button + * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned + * @param {string} [props.newLabel] new label for quick reply (text on the button) + * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply + * @param {string} [props.title] the title / tooltip to be shown on the quick reply button + * @param {boolean} [props.isHidden] whether to hide or show the button + * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected + * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated * @returns {QuickReply} the altered quick reply */ updateQuickReply(setName, label, { + icon, + showLabel, newLabel, message, title, @@ -259,6 +279,8 @@ export class QuickReplyApi { if (!qr) { throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); } + qr.updateIcon(icon ?? qr.icon); + qr.updateShowLabel(showLabel ?? qr.showLabel); qr.updateLabel(newLabel ?? qr.label); qr.updateMessage(message ?? qr.message); qr.updateTitle(title ?? qr.title); @@ -276,8 +298,8 @@ export class QuickReplyApi { /** * Deletes an existing quick reply. * - * @param {String} setName name of the existing quick reply set - * @param {String} label label of the existing quick reply (text on the button) + * @param {string} setName name of the existing quick reply set + * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID */ deleteQuickReply(setName, label) { const qr = this.getQrByLabel(setName, label); @@ -291,10 +313,10 @@ export class QuickReplyApi { /** * Adds an existing quick reply set as a context menu to an existing quick reply. * - * @param {String} setName name of the existing quick reply set containing the quick reply - * @param {String} label label of the existing quick reply - * @param {String} contextSetName name of the existing quick reply set to be used as a context menu - * @param {Boolean} isChained whether or not to chain the context menu quick replies + * @param {string} setName name of the existing quick reply set containing the quick reply + * @param {string|number} label label of the existing quick reply or its numeric ID + * @param {string} contextSetName name of the existing quick reply set to be used as a context menu + * @param {boolean} isChained whether or not to chain the context menu quick replies */ createContextItem(setName, label, contextSetName, isChained = false) { const qr = this.getQrByLabel(setName, label); @@ -314,9 +336,9 @@ export class QuickReplyApi { /** * Removes a quick reply set from a quick reply's context menu. * - * @param {String} setName name of the existing quick reply set containing the quick reply - * @param {String} label label of the existing quick reply - * @param {String} contextSetName name of the existing quick reply set to be used as a context menu + * @param {string} setName name of the existing quick reply set containing the quick reply + * @param {string|number} label label of the existing quick reply or its numeric ID + * @param {string} contextSetName name of the existing quick reply set to be used as a context menu */ deleteContextItem(setName, label, contextSetName) { const qr = this.getQrByLabel(setName, label); @@ -333,8 +355,8 @@ export class QuickReplyApi { /** * Removes all entries from a quick reply's context menu. * - * @param {String} setName name of the existing quick reply set containing the quick reply - * @param {String} label label of the existing quick reply + * @param {string} setName name of the existing quick reply set containing the quick reply + * @param {string|number} label label of the existing quick reply or its numeric ID */ clearContextMenu(setName, label) { const qr = this.getQrByLabel(setName, label); @@ -348,11 +370,11 @@ export class QuickReplyApi { /** * Create a new quick reply set. * - * @param {String} name name of the new quick reply set - * @param {Object} [props] - * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box - * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input - * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @param {string} name name of the new quick reply set + * @param {object} [props] + * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply * @returns {Promise} the new quick reply set */ async createSet(name, { @@ -384,11 +406,11 @@ export class QuickReplyApi { /** * Update an existing quick reply set. * - * @param {String} name name of the existing quick reply set - * @param {Object} [props] - * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box - * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input - * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @param {string} name name of the existing quick reply set + * @param {object} [props] + * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply * @returns {Promise} the altered quick reply set */ async updateSet(name, { @@ -411,7 +433,7 @@ export class QuickReplyApi { /** * Delete an existing quick reply set. * - * @param {String} name name of the existing quick reply set + * @param {string} name name of the existing quick reply set */ async deleteSet(name) { const set = this.getSetByName(name); @@ -451,7 +473,7 @@ export class QuickReplyApi { /** * Gets a list of all quick replies in the quick reply set. * - * @param {String} setName name of the existing quick reply set + * @param {string} setName name of the existing quick reply set * @returns array with the labels of this set's quick replies */ listQuickReplies(setName) { diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index b9ce236d6..89766d78a 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -2,10 +2,23 @@

Labels and Message

-
@@ -43,6 +58,10 @@ +
+ + +

Context Menu

@@ -64,7 +83,7 @@

Auto-Execute

-
+
-
+ +
+ + + + + + +
+ +
diff --git a/public/scripts/extensions/quick-reply/html/settings.html b/public/scripts/extensions/quick-reply/html/settings.html index c618bddf3..6c3845ca5 100644 --- a/public/scripts/extensions/quick-reply/html/settings.html +++ b/public/scripts/extensions/quick-reply/html/settings.html @@ -60,10 +60,20 @@ +
+ + + Color +
+
+ +
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index ab6044bf0..7bd108243 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -176,7 +176,7 @@ const init = async () => { buttons.show(); settings.onSave = ()=>buttons.refresh(); - window['executeQuickReplyByName'] = async(name, args = {}) => { + window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => { let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])] .map(it=>it.set.qrList) .flat() @@ -191,7 +191,7 @@ const init = async () => { } } if (qr && qr.onExecute) { - return await qr.execute(args, false, true); + return await qr.execute(args, false, true, options); } else { throw new Error(`No Quick Reply found for "${name}".`); } diff --git a/public/scripts/extensions/quick-reply/lib/morphdom-esm.js b/public/scripts/extensions/quick-reply/lib/morphdom-esm.js new file mode 100644 index 000000000..7a13a27fc --- /dev/null +++ b/public/scripts/extensions/quick-reply/lib/morphdom-esm.js @@ -0,0 +1,769 @@ +var DOCUMENT_FRAGMENT_NODE = 11; + +function morphAttrs(fromNode, toNode) { + var toNodeAttrs = toNode.attributes; + var attr; + var attrName; + var attrNamespaceURI; + var attrValue; + var fromValue; + + // document-fragments dont have attributes so lets not do anything + if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { + return; + } + + // update attributes on original DOM element + for (var i = toNodeAttrs.length - 1; i >= 0; i--) { + attr = toNodeAttrs[i]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + attrValue = attr.value; + + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); + + if (fromValue !== attrValue) { + if (attr.prefix === 'xmlns'){ + attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix + } + fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); + } + } else { + fromValue = fromNode.getAttribute(attrName); + + if (fromValue !== attrValue) { + fromNode.setAttribute(attrName, attrValue); + } + } + } + + // Remove any extra attributes found on the original DOM element that + // weren't found on the target element. + var fromNodeAttrs = fromNode.attributes; + + for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { + attr = fromNodeAttrs[d]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + + if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { + fromNode.removeAttributeNS(attrNamespaceURI, attrName); + } + } else { + if (!toNode.hasAttribute(attrName)) { + fromNode.removeAttribute(attrName); + } + } + } +} + +var range; // Create a range object for efficently rendering strings to elements. +var NS_XHTML = 'http://www.w3.org/1999/xhtml'; + +var doc = typeof document === 'undefined' ? undefined : document; +var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template'); +var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange(); + +function createFragmentFromTemplate(str) { + var template = doc.createElement('template'); + template.innerHTML = str; + return template.content.childNodes[0]; +} + +function createFragmentFromRange(str) { + if (!range) { + range = doc.createRange(); + range.selectNode(doc.body); + } + + var fragment = range.createContextualFragment(str); + return fragment.childNodes[0]; +} + +function createFragmentFromWrap(str) { + var fragment = doc.createElement('body'); + fragment.innerHTML = str; + return fragment.childNodes[0]; +} + +/** + * This is about the same + * var html = new DOMParser().parseFromString(str, 'text/html'); + * return html.body.firstChild; + * + * @method toElement + * @param {String} str + */ +function toElement(str) { + str = str.trim(); + if (HAS_TEMPLATE_SUPPORT) { + // avoid restrictions on content for things like `Hi` which + // createContextualFragment doesn't support + //