From 6b494af146c415185d1dbc0b8ac2add003bff741 Mon Sep 17 00:00:00 2001 From: Eradev Date: Wed, 15 Jan 2025 00:19:47 -0500 Subject: [PATCH 01/83] Fix the date for featherless.ai models --- public/scripts/textgen-models.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 8f4366333..3ccbb6871 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -383,7 +383,7 @@ export async function loadFeatherlessModels(data) { const dateAddedDiv = document.createElement('div'); dateAddedDiv.classList.add('model-date-added'); - dateAddedDiv.textContent = `Added On: ${new Date(model.updated_at).toLocaleDateString()}`; + dateAddedDiv.textContent = `Added On: ${new Date(model.created * 1000).toLocaleDateString()}`; detailsContainer.appendChild(modelClassDiv); detailsContainer.appendChild(contextLengthDiv); @@ -502,9 +502,9 @@ export async function loadFeatherlessModels(data) { } else if (selectedSortOrder === 'desc') { filteredModels.sort((a, b) => b.id.localeCompare(a.id)); } else if (selectedSortOrder === 'date_asc') { - filteredModels.sort((a, b) => a.updated_at.localeCompare(b.updated_at)); + filteredModels.sort((a, b) => a.created - b.created); } else if (selectedSortOrder === 'date_desc') { - filteredModels.sort((a, b) => b.updated_at.localeCompare(a.updated_at)); + filteredModels.sort((a, b) => b.created - a.created); } setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage); From c1535f1b34c1c8ac9a614b5239ed53dd2fdea3bc Mon Sep 17 00:00:00 2001 From: Eradev Date: Wed, 15 Jan 2025 00:35:40 -0500 Subject: [PATCH 02/83] Fixed class filter & new models count in featherless.ai --- public/scripts/textgen-models.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 3ccbb6871..278d05103 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -472,6 +472,7 @@ export async function loadFeatherlessModels(data) { featherlessTop = await fetchFeatherlessStats(); } const featherlessIds = featherlessTop.map(stat => stat.id); + if (selectedCategory === 'New') { featherlessNew = await fetchFeatherlessNew(); } @@ -493,7 +494,7 @@ export async function loadFeatherlessModels(data) { return matchesSearch && matchesClass && matchesNew; } else { - return matchesSearch; + return matchesSearch && matchesClass; } }); @@ -528,7 +529,7 @@ async function fetchFeatherlessStats() { } async function fetchFeatherlessNew() { - const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=10'); + const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=20'); const data = await response.json(); return data.items; } From a06270fc858d9a51aea188a45b0fa8c37ea51b3a Mon Sep 17 00:00:00 2001 From: Eradev Date: Wed, 15 Jan 2025 08:04:15 -0500 Subject: [PATCH 03/83] Fixed display bug when selecting a model --- public/scripts/textgen-models.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 278d05103..824d5eec4 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -319,8 +319,6 @@ export async function loadFeatherlessModels(data) { return; } - // Sort the data by model id (default A-Z) - data.sort((a, b) => a.id.localeCompare(b.id)); originalModels = data; // Store the original data for search featherlessModels = data; @@ -334,10 +332,8 @@ export async function loadFeatherlessModels(data) { // Retrieve the stored number of items per page or default to 10 const perPage = Number(localStorage.getItem(storageKey)) || 10; - // Initialize pagination with the full set of models - const currentModelIndex = data.findIndex(x => x.id === textgen_settings.featherless_model); - featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1; - setupPagination(originalModels, perPage); + // Initialize pagination + applyFiltersAndSort(); // Function to set up pagination (also used for filtered results) function setupPagination(models, perPage, pageNumber = featherlessCurrentPage) { @@ -508,6 +504,9 @@ export async function loadFeatherlessModels(data) { filteredModels.sort((a, b) => b.created - a.created); } + const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model); + featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1; + setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage); } From 94d69b764a7fd3f1131b9f9f1bb06f8ad1812d28 Mon Sep 17 00:00:00 2001 From: Rivelle Date: Thu, 16 Jan 2025 02:02:48 +0800 Subject: [PATCH 04/83] Update zh-tw Translation - Update translations - Minor adjustments and terminology unification --- public/locales/zh-tw.json | 74 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 48edfbbaa..e5ef3a5a9 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -880,7 +880,7 @@ "popup-button-no": "取消", "popup-button-cancel": "關閉", "popup-button-import": "匯入", - "Advanced Definitions": "- 進階定義", + "Advanced Defininitions": "- 進階定義", "Prompt Overrides": "提示詞覆寫", "(For Chat Completion and Instruct Mode)": "(用於聊天補全和指令模式)", "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。", @@ -1745,7 +1745,7 @@ "Sprite Folder Override": "表情立繪資料夾覆蓋", "Sprite set:": "立繪組:", "Show Gallery": "查看圖庫", - "Sticky": "固定", + "Sticky": "黏性", "Style Preset": "預設樣式", "Summarize chat messages for vector generation": "摘要聊天訊息以進行向量化處理", "Summarize chat messages when sending": "傳送時摘要聊天內容", @@ -2124,7 +2124,7 @@ "from other websites": "以從其他網站新增。", "Go to the": "前往", "to install additional features.": "以安裝更多功能。", - "If you're connected to an API, try asking me something!": "若您已連接 API,嘗試問我一些問題吧!", + "If you're connected to an API, try asking me something!": "若您已連線 API,嘗試問我一些問題吧!", "Title/Memo": "標題/備註", "Strategy": "插入策略", "Position": "位置", @@ -2386,5 +2386,71 @@ "Your preset contains proxy and/or custom endpoint settings.": "此預設包含代理和/或自訂端點設定。", "Do you want to remove these fields before exporting?": "是否要在匯出前移除這些欄位?", "Delete the preset? This action is irreversible and your current settings will be overwritten.": "確定刪除此預設?刪除後無法復原,且此設定將被覆蓋。", - "Update all": "全部更新" + "Update all": "全部更新", + "Automatically chooses an alternative provider if chosen providers can't serve your request.": "當所選提供者無法滿足您的請求時,自動選擇替代提供者。", + "Use extension settings": "使用擴充功能設定", + "To use instruct formatting, switch to OpenRouter under Text Completion API.": "若要使用指令格式,請在文本補全 API 下切換至 OpenRouter。", + "Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.": "如果模型在達到一定數量的符元前停止,則自動繼續生成回應。", + "Toggle entry's active state.": "切換條目的啟用狀態。", + "Non-sticky": "無黏性", + "No cooldown": "無冷卻時間", + "No delay": "無延遲", + "Included settings:": "包含設定:", + "Click on the setting name to omit it from the profile.": "點擊設定名稱以從設定檔中省略。", + "Tints the chat background and/or character sprites.": "調整聊天背景或角色圖片的色調。", + "Only chunk on custom boundary": "僅在自定邊界進行分塊(chunk)", + "help_macros_firstDisplayedMessageId": "載入到可見聊天中的第一則訊息的 ID。", + "Couldn't import tags:": "無法匯入標籤:", + "Select providers. No selection = all providers.": "選擇供應商。未選擇=所有供應商。", + "Select a model": "選擇模型", + "Search models...": "搜尋模型⋯", + "[Currently loaded]": "[當前加載]", + "Search providers...": "搜尋供應商⋯", + "No-sticky": "無固定", + "Create a new World Info": "建立新世界資訊", + "Enter a name for the new file:": "輸入新檔案的名稱:", + "Valid World Info file name is required": "需要有效的世界資訊檔案名稱", + "World Info file has an invalid format": "世界資訊檔案格式無效", + "World Info file has no entries": "世界資訊檔案中無任何條目", + "Character not found.": "未找到該角色。", + "Open a chat to get a name of the chat-bound lorebook": "開啟聊天以取得綁定聊天的知識書名稱。", + "File is not valid: ${0}": "檔案無效或格式不正確:${0}", + "The world with ${0} is invalid or corrupted.": "世界 ${0} 無效或已損壞。", + "Deactivated all worlds": "已停用所有世界", + "No world found named: ${0}": "未找到名為 ${0} 的世界", + "Activated world: ${0}": "已啟用世界:${0}", + "Deactivated world: ${0}": "已停用世界:${0}", + "World was not active: ${0}": "世界 ${0} 未啟用", + "The world '${0}' has been imported and linked to the character successfully.": "世界「${0}」已成功匯入並綁定至角色。", + "World/Lorebook imported": "世界/知識書已匯入", + "Are you sure you want to import '${0}'?": "確定要匯入「${0}」嗎?", + "Built-in Extensions:": "內建擴充功能:", + "Installed Extensions:": "已安裝的擴充功能:", + "Loading third-party extensions... Please wait...": "正在載入第三方擴充功能,請稍候⋯", + "The page will be reloaded shortly...": "頁面即將重新載入⋯", + "Extensions state changed": "擴充功能狀態已更改", + "Error loading extensions. See browser console for details.": "載入擴充功能時出現錯誤。詳情請查看瀏覽器控制台。", + "You don't have permission to update global extensions.": "您無權更新全域擴充功能。", + "Extension update failed": "擴充功能更新失敗", + "Extension ${0} updated to ${1}": "擴充功能 ${0} 已更新至 ${1}", + "Reload the page to apply updates": "重新加載頁面以使用更新", + "You don't have permission to delete global extensions.": "您無權刪除全域擴充功能。", + "Are you sure you want to delete ${0}?": "確定要刪除 ${0} 嗎?", + "You don't have permission to move extensions.": "您無權移動擴充功能。", + "Are you sure you want to move ${0} to your local extensions? This will make it available only for you.": "確定要將 ${0} 移至本地擴充功能嗎?此後僅您可使用。", + "Are you sure you want to move ${0} to the global extensions? This will make it available for all users.": "確定要將 ${0} 移至全域擴充功能嗎?此後所有使用者皆可使用。", + "Extension ${0} moved.": "擴充功能 ${0} 已移動。", + "Extension ${0} deleted": "擴充功能 ${0} 已刪除。", + "Please wait...": "請稍候⋯", + "Installing extension": "正在安裝擴充功能", + "Extension installation failed": "擴充功能安裝失敗", + "Extension '${0}' by ${1} (version ${2}) has been installed successfully!": "擴充功能 '${0}' 由 ${1} 提供(版本 ${2})已成功安裝!", + "Extension installation successful": "擴充功能安裝成功", + "Extension updates available": "有可用的擴充功能更新", + "Auto-updating extensions. This may take several minutes.": "正在自動更新擴充功能。這可能需要幾分鐘時間。", + "Install": "安裝", + "Modules provided by your Extras API:": "由您的 Extras API 提供的模組:", + "Not connected to the API!": "未連線到 API!", + "ext_type_system": "這是內建的擴充功能,無法刪除,且會跟隨系統更新。", + "Valid": "已驗證" } From 8acba006559952995c29d6868c6dd4bbb56083f5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:02:10 +0200 Subject: [PATCH 05/83] Upscale default monocolor BGs --- default/content/backgrounds/_black.jpg | Bin 8049 -> 8261 bytes default/content/backgrounds/_white.jpg | Bin 7635 -> 8261 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/default/content/backgrounds/_black.jpg b/default/content/backgrounds/_black.jpg index a451bc161f641ffc1ee52c854dce378fff0b3545..30ced914c6b90823b7f0685a64634c04b9903854 100644 GIT binary patch literal 8261 zcmeIuy$!-J5QgD%5{&Kq*$H$ELKh^YNH7&kunz@8@YAq~vCGiYz28ZeeezxL#7F#y zKhLLGlqzJGO5$6NQWiP+EK@37y?y8C#O z+En}XMX4b1F59$Uma(b5@xNV(FERP=B-g`ofENM?AbKN~ zz6{3aAK?2Z2IKS3_mPhT-}WCq!KqXQ-*(Cg7~e^i^14Sel3iWt0Y4hZ0jV~aS$)I%zAqTQki;x*=6#Nji3|UDXp*-~qqrQ_`+n_N4O%Oqj zehHsUg-E>@>IZM)k)_f1At>KM9sjxxp*7ulIm}9V&NEq(R7ojG6;CMGAM&i7=0#ST zRjxpxKq#CHM3cd|Cm2Zv;>mCbxFBS{m(s?r*AcqcWQ;-GYi`yFB|q1Oste_9 z>O!q+)x|{VJgAV#;B8vm;5QB>SOt-?ezN`rauF@AO=pm){&&|5H?)7R6p)a@dvQD}|xb810n=Yp;U-CbSXyFtlc-ddQs@s0tF3#R`=Yvnb^6n?!x{=8Q(prd2cAGUyL|7hN zWC00wdwZ;*eFuB71jn-{&V*OyPs{rcrKkEBC31F^4GUmj9ew@{5aPZi?C6%5%Dr!h z@Z%D!X^K#P54p@_Og9=rUmiO&v1+J(s{Tc1chjfC{Q(qDO=K*qhCE=uYS#D-W?}L{*3@`Wi^iKPDldr&;Fn;^ zbVU_)B|yo%Y&5b(C1W(wvY2i*M77!2m=_cSv#F@g7>&`gFk&>oj!o7nW>>Lc2+c|; zJC;g=G&ne-EbH;GQZ>I``YUddG$xA|z*546bSat5Z4uQCE3yKfTO*|e)-xA8s|YHA zPu7I>S%bV|hGSGo9-&nlYe0mYEKVxASKvA(h7ya7HgW}x&3VJ-Aok8R>=Ywt@JX3j zza4WRshg3d%5;f?$psJP#iDM$E{w>6OeslKrLrK2#YPvGj=>_dz!&K7EMF|OB06;h z$Y#*vwPOB4lR=BX;CEU?rpP*4O>kX}L7^DB4r%_jfWYu>w1ZGyX0$IST+flVljM+5 zq!;W|ICV4@34jUh*91C_{DlHw1ju1;fkZVRNAAAIfZ!dALOUSWX`%#zJ--1VJimiB z!YivjXEJ|^(Q~5e{|S!R!hw0~>(I0w)+KxmyIqcJWJlEOxC}SV!;opx<0jTn`)uf1 zG>b}zhd6=bQDfO)5+k+3^nL~sS6T^lL^DVru`1wLh7TOJyi^rg<-9e@s6oKtsYckb z4ex>WdESRoO)n**W}8lk+crhu8BW#@K``sRcVY9Ihj^D762%>r=kb=4viQ?FlM4$^g!|7*56!}g|=;i z7{48oQHt$biH#Q299?Xq>S8!S$Z<7wLK)9xHtNC3p@Z9qoh3=$J1p=;-BJrR<;Qg} zXl`e@0$nA+(>cS*>Kx=-b#Oa*XTF%01W9f&u3O`u8Z!{k1#OZ@ zAwU?r0P4z%=Yw1t(MDbIY;!plSHPzu>5dh7k(~ygUSK4;=SOP*>qo zNDVe5)cX?@9uz_W@?mErV`zJW9qy83l=|te-JNr|!B{d7Nd^ZzjpT*VWVVpZj%;X*1+77EL#f%>+5XvZza$s^ z!9*hA4}|=okPj$)${tarYd%p~p>r7F6h`LB>DGfe!pW_w4thaSO@(sBU^}O+bV8Of z=qeDV$>1#G=LD|IiK+s24Py0mrkRXEJ}299*`QX`(HW5FViTs|=;_aLid2;uZgd7l zT%iwE(XaD!voO$P=5eP=GA(m7oMUrvhSevkd4X#GQ=p{-2j^aH(2M^i@CBm2U_7V& zOAv_uDv(G90vc%s{D6f6@{bUQLv6$o%NO?iBq_@z3zA%>!Ag9YE^_{z6|P8z%Vd%h zqN36w!%d6~dO^8(qrd_doEUV*vW z(^w!=-0ntA;622Xk~D*_a)Vw5&oak)8(@A8J6)PY8rxa8fLwI;H8MpisI#=p4HvfF>Zo1S0V;c;rPzZ~EfT5xv2i zDbZq)gUIRk{(NOF&0qiL%J^}>)I7|8zVi`WOK^pWBp`^`xN2+_SLq>rzMQwfyanbh zFmHi*3(Q;K|HuN(%O)qnIcXNIk}VH+>+zM9Jltu|VwaSgn#pXWmY(>T%48rewbJr% z2QOXi4|hjFOq&TFpNwR8*sK<(xdR?B;o%P1%rL%z1eDosa!^jx(bnL{Dn6^xFTLSS6!D#j!#^@ZTpU5 ziQhRhTidhuh8u6X`IcL6yZw$k@4EY*WA{Js;6o2T^2C!*J^jqH&%OB4%dfop+Usw< z{m#25PrdiyM<0Lk>1UsR_4PO3e)s(kpgw+Tw3w|Hi^XQMmO*4UJT+Q_HstLYzQ!J+ zdv`lh`&Jz6%O1%u4m%%RdHh6LKC|TX{s_D{E@ei}9(`UGIJHt{KL`kLm8? zO=?r^*B7ON#JgP$5v&9xY@OJ`P9Y%OE$I>1V)xI@RJ(NjO2mQjF=hOf-laH zeK1d#_)= z_xkngZguf&aniiLufV4fipS9kgpdQ-soBU3H41)+nvblcj!=sFjZxo1Ei2KOfF_6_ zN56$nrb49N2=xQ|d1NW|eG$q}kn5j25n5WWmp!bM;=Ds+k}4@B;7sm`R}J*3MpDRR z){*syvQTD|#bmYH99FYC3`mdJvS4=5+OwW+@NL@_vNaw!{7Q7;qNc=&RJc{%eI{mK zd~o>6P<9#z}td^B>N`~J1P?|huUaBcO{$DZAH?D12deD%X~ zufO~0#p@f=;}1UZ{2T9m_Vte){iB(Q{ZGE|=KG(2bHjs7CXm)bWU||=ZA63x!Pyp& zV4JVO8rpTB5le6)app>RQEF1&eK0oM#3-#-7u&D^_9bm6KLR0+&SCpT+EwnnA;Q0x zU};%|;upwmCS!WgN_0K>#W`sIJSaxx5NZyb95fq7J7dNgx!1qRMe%K8q2!j?0)Jh$7q^bVB2>U@b$Ei$j8!6PbOgv| z(BrjYzO2chg<$YIEhJN99W5cay3(LftlWt-f2$=hyd7;Ol=F=C-Gu8k(sq&@GK%zq ztqLbsX^{Y!&|W9d0pw4o0V6;Tn+qhW1UdZlGX^9?;F}@WY49dSA?*1j2;q4X+6b?# z;OH@ng=1%q{mIHkM_CH2ofPi z@bC!`Em+VqSi)ht%2ZlX1f=(`#1p1f0BDqh1rqZDj%oPhVM|GQkyWOwRYt`_<0(hj z!3{5h_7pEf(XzJ^u@Yrx#BG}*@C>JH5Z2)3L{*JrB{%@jhR;qKgS{1F*)y_~&sBkJ zlFVm#BU1JaWA5t+40zPic~$D?L{6qvjs*ZpZqMm~-lY*^2*wO%GhQhE*ZQ0DvQV{M z5aU-v;!37^OR>>{nxr#TRCNp|2uZG>4k`o5_!>QEIdpIpv2#q4ck~E+Mz_=~P5A*G z44T_nE=}i25Ot1ovN{d<1|3{YUX#kiBteoZjO)?(NBa!~bU|w+5!>3SN;%k!6|OQ$ zxdaGjw?N$~68>(1*T58yO(iEQa2Xy5c3W`gl~+hzTWuavr?hBf!G~(ip|xm->5(Z6 znZAB-i5(t3II_-#Gv3S*b>@hA>k(D+t_9#J2#vxA9tMX-)B^!qLMaH`UO4!TL0yGU zAvM^VP~&YVyeNbM~O6lqZB9IH^x*o7xDW=rG>^9z!pZz`U`ZP;;<75@?MCJG`ajgVAI*9ZB@AE{z4PZeK~MiHV7piExV~ zXZ*pgt}cHdzu93=Xs2~%+V^d~q)%F7Jb zHx45%)CbGyH~Z;X7-%x{xKl0?lQ|krv`IM4>JwGIN>%?qP|<;d6EN59!+#hw2ils0 zok{%$kr3P35$*t=2zG?%=GJg)xS3`;I-A4kP9_~@QyuLAPVe6pB zkHPcGvAz!BU7ZbIzh`^PX?@bTYvI{@P51yH9h=ORV28IMXQrT zB*0Z81l_BLikWi80y7qvvA~Q4W-Kscf&U{5lrJ%y2sf7rxS~|N4XwggA@Vk)y}aB} zZYn3Ukt%xPmk^VIxYJ6-+YmgaRKE=!0WobRc;7IR-C?s@oMsoik-^&#vYBCg0|_Xz z-Q=L0$W>F1Oq3a3hs;B z#j(S$Tv^g8yXKuZ)3$VSxPAB4gGYLkO5f^^erDOohSB-2p8V+i_v{|^(Y@T!*WUT~ z!t&1awfu^%$iU!z8#isvjPYB>Ckoql?0azkLk~al*yB$;`P9?TJp0`9FTD8D%g0`S ztUL&7Xb-_3>+##cZ`$EH;}p z3nF{qHOm^bAzwq!dV7d&+~$bxT6mx-aX58X*!jw$6K7)bl{u5UTj5b_F4KGU$g4}* z<|W(v*rhII&!ekVEbj=H-_Wsl9})Q4^7)TFdkjBoJ+8fLE$bgmcN$My<#(-tjbnv} zo_g`E56)h?*)_O{-~RB^FTMTYxo>}35xH;kmK~2g^YZc2Uwrp-_4Cu07rs1x Date: Wed, 15 Jan 2025 21:15:25 +0200 Subject: [PATCH 06/83] Add RisuAI chats JSON import --- src/endpoints/chats.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index 62c790400..2df194797 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -264,6 +264,34 @@ function flattenChubChat(userName, characterName, lines) { return (lines ?? []).map(convert).join('\n'); } +/** + * Imports a chat from RisuAI format. + * @param {string} userName User name + * @param {string} characterName Character name + * @param {object} jsonData Imported chat data + * @returns {string} Chat data + */ +function importRisuChat(userName, characterName, jsonData) { + /** @type {object[]} */ + const chat = [{ + user_name: userName, + character_name: characterName, + create_date: humanizedISO8601DateTime(), + }]; + + for (const message of jsonData.data.message) { + const isUser = message.role === 'user'; + chat.push({ + name: message.name ?? (isUser ? userName : characterName), + is_user: isUser, + send_date: Number(message.time ?? Date.now()), + mes: message.data ?? '', + }); + } + + return chat.map(obj => JSON.stringify(obj)).join('\n'); +} + export const router = express.Router(); router.post('/save', jsonParser, function (request, response) { @@ -481,6 +509,8 @@ router.post('/import', urlencodedParser, function (request, response) { importFunc = importOobaChat; } else if (Array.isArray(jsonData.messages)) { // Agnai's format importFunc = importAgnaiChat; + } else if (jsonData.type === 'risuChat') { // RisuAI format + importFunc = importRisuChat; } else { // Unknown format console.log('Incorrect chat format .json'); return response.send({ error: true }); From 0441a8725f14892d4e3b5e3d4c67f94767bb6bc6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:58:49 +0200 Subject: [PATCH 07/83] WebSearch: Add KoboldCpp search endpoint. --- src/endpoints/search.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/endpoints/search.js b/src/endpoints/search.js index e29e53008..c999f8264 100644 --- a/src/endpoints/search.js +++ b/src/endpoints/search.js @@ -4,6 +4,8 @@ import express from 'express'; import { decode } from 'html-entities'; import { readSecret, SECRET_KEYS } from './secrets.js'; import { jsonParser } from '../express-common.js'; +import { trimV1 } from '../util.js'; +import { setAdditionalHeaders } from '../additional-headers.js'; export const router = express.Router(); @@ -257,6 +259,41 @@ router.post('/tavily', jsonParser, async (request, response) => { } }); +router.post('/koboldcpp', jsonParser, async (request, response) => { + try { + const { query, url } = request.body; + + if (!url) { + console.error('No URL provided for KoboldCpp search'); + return response.sendStatus(400); + } + + console.debug('KoboldCpp search query', query); + + const baseUrl = trimV1(url); + const args = { + method: 'POST', + headers: {}, + body: JSON.stringify({ q: query }), + }; + + setAdditionalHeaders(request, args, baseUrl); + const result = await fetch(`${baseUrl}/api/extra/websearch`, args); + + if (!result.ok) { + const text = await result.text(); + console.error('KoboldCpp request failed', result.statusText, text); + return response.status(500).send(text); + } + + const data = await result.json(); + return response.json(data); + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +}); + router.post('/visit', jsonParser, async (request, response) => { try { const url = request.body.url; From 2723d3b73b13d08037ebb44c5bfc67b653200386 Mon Sep 17 00:00:00 2001 From: Rivelle Date: Thu, 16 Jan 2025 04:20:22 +0800 Subject: [PATCH 08/83] Update zh-tw.json --- public/locales/zh-tw.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index e5ef3a5a9..10ac9e1cb 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -28,14 +28,14 @@ "Prose Augmenter": "散文增強器", "Text Adventure": "文字冒險", "response legth(tokens)": "回應長度(符元數)", - "Streaming": "串流", - "Streaming_desc": "生成時逐位顯示回應。當此功能關閉時,回應將在完成後一次顯示。", + "Streaming": "即時串流", + "Streaming_desc": "逐字顯示生成中的回應內容。關閉時,回應將在生成完成後一次性顯示。", "context size(tokens)": "上下文長度(符元數)", "unlocked": "解鎖", "Only enable this if your model supports context sizes greater than 8192 tokens": "僅在您的模型支援超過 8192 個符元的上下文長度時啟用此功能", - "Max prompt cost:": "最大提示詞費用", - "Display the response bit by bit as it is generated.": "生成時逐位顯示回應。", - "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在完成後一次性顯示。", + "Max prompt cost:": "最大提示詞費用:", + "Display the response bit by bit as it is generated.": "逐字顯示生成中的回應內容。", + "When this is off, responses will be displayed all at once when they are complete.": "關閉時,回應將在生成完成後一次性顯示。", "Temperature": "溫度", "rep.pen": "重複懲罰", "Rep. Pen. Range.": "重複懲罰範圍", @@ -880,7 +880,7 @@ "popup-button-no": "取消", "popup-button-cancel": "關閉", "popup-button-import": "匯入", - "Advanced Defininitions": "- 進階定義", + "Advanced Definitions": "- 進階定義", "Prompt Overrides": "提示詞覆寫", "(For Chat Completion and Instruct Mode)": "(用於聊天補全和指令模式)", "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。", From af987143b7d511c709baa7b82b13e77f39d9fa35 Mon Sep 17 00:00:00 2001 From: Rivelle Date: Thu, 16 Jan 2025 04:23:26 +0800 Subject: [PATCH 09/83] Update zh-tw.json --- public/locales/zh-tw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 10ac9e1cb..5d8313c1d 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -880,7 +880,7 @@ "popup-button-no": "取消", "popup-button-cancel": "關閉", "popup-button-import": "匯入", - "Advanced Definitions": "- 進階定義", + "Advanced Definitions": "進階定義", "Prompt Overrides": "提示詞覆寫", "(For Chat Completion and Instruct Mode)": "(用於聊天補全和指令模式)", "Insert {{original}} into either box to include the respective default prompt from system settings.": "在任一框中插入 {{original}} 以包含系統設定中的預設提示詞。", From f98d27f1870a6e9c9ca257e6ad87afd7b33fcf2c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:59:41 +0200 Subject: [PATCH 10/83] Add sorting order for extensions manager --- public/scripts/extensions.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index e8da94a6f..c0ba05067 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -38,7 +38,8 @@ export let modules = []; let activeExtensions = new Set(); const getApiUrl = () => extension_settings.apiUrl; -const sortManifests = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name)); +const sortManifestsByOrder = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name)); +const sortManifestsByName = (a, b) => String(a.display_name).localeCompare(String(b.display_name)) || parseInt(a.loading_order) - parseInt(b.loading_order); let connectedToApi = false; /** @@ -355,7 +356,7 @@ async function getManifests(names) { * @returns {Promise} */ async function activateExtensions() { - const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1])); + const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1])); const promises = []; for (let entry of extensions) { @@ -712,7 +713,10 @@ async function showExtensionsDetails() { htmlExternal.append(htmlLoading); - const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1])).map(getExtensionData); + const sortOrderKey = 'extensions_sortByName'; + const sortByName = localStorage.getItem(sortOrderKey) === 'true'; + const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder; + const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData); extensions.forEach(value => { const { isExternal, extensionHtml } = value; @@ -729,7 +733,6 @@ async function showExtensionsDetails() { /** @type {import('./popup.js').CustomPopupButton} */ const updateAllButton = { text: t`Update all`, - appendAtEnd: true, action: async () => { requiresReload = true; await autoUpdateExtensions(true); @@ -737,13 +740,23 @@ async function showExtensionsDetails() { }, }; + /** @type {import('./popup.js').CustomPopupButton} */ + const sortOrderButton = { + text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`, + action: async () => { + abortController.abort(); + localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true'); + await showExtensionsDetails(); + }, + }; + let waitingForSave = false; const popup = new Popup(html, POPUP_TYPE.TEXT, '', { okButton: t`Close`, wide: true, large: true, - customButtons: [updateAllButton], + customButtons: [sortOrderButton, updateAllButton], allowVerticalScrolling: true, onClosing: async () => { if (waitingForSave) { @@ -762,7 +775,7 @@ async function showExtensionsDetails() { }); popupPromise = popup.show(); popup.content.scrollTop = initialScrollTop; - checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); + checkForUpdatesManual(sortFn, abortController.signal).finally(() => htmlLoading.remove()); } catch (error) { toastr.error(t`Error loading extensions. See browser console for details.`); console.error(error); @@ -1073,12 +1086,13 @@ function processVersionCheckQueue() { /** * Performs a manual check for updates on all 3rd-party extensions. + * @param {function} sortFn Sort function * @param {AbortSignal} abortSignal Signal to abort the operation * @returns {Promise} */ -async function checkForUpdatesManual(abortSignal) { +async function checkForUpdatesManual(sortFn, abortSignal) { const promises = []; - for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortManifests(manifests[a], manifests[b]))) { + for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortFn(manifests[a], manifests[b]))) { const externalId = id.replace('third-party', ''); const promise = enqueueVersionCheck(async () => { try { @@ -1223,7 +1237,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) { exitImmediately = immediately; }; - for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifests(a, b))) { + for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifestsByOrder(a, b))) { const interceptorKey = manifest.generate_interceptor; if (typeof globalThis[interceptorKey] === 'function') { try { From 6e2b5d5dc8aed6e30ab5cbc5b4e5da4c8a2ca3e2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:55:26 +0200 Subject: [PATCH 11/83] Fix copy actions for non-HTTPS/localhost Fixes #2352 --- public/script.js | 59 ++++++++++++++++++----------------------- public/scripts/utils.js | 20 ++++++++++++++ 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/public/script.js b/public/script.js index e8e04bbd8..14d695611 100644 --- a/public/script.js +++ b/public/script.js @@ -168,6 +168,7 @@ import { isTrueBoolean, toggleDrawer, isElementInViewport, + copyText, } from './scripts/utils.js'; import { debounce_timeout } from './scripts/constants.js'; @@ -2315,16 +2316,15 @@ export function addCopyToCodeBlocks(messageElement) { const codeBlocks = $(messageElement).find('pre code'); for (let i = 0; i < codeBlocks.length; i++) { hljs.highlightElement(codeBlocks.get(i)); - if (navigator.clipboard !== undefined) { - const copyButton = document.createElement('i'); - copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable'); - copyButton.title = 'Copy code'; - codeBlocks.get(i).appendChild(copyButton); - copyButton.addEventListener('pointerup', function (event) { - navigator.clipboard.writeText(codeBlocks.get(i).innerText); - toastr.info(t`Copied!`, '', { timeOut: 2000 }); - }); - } + const copyButton = document.createElement('i'); + copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable'); + copyButton.title = 'Copy code'; + codeBlocks.get(i).appendChild(copyButton); + copyButton.addEventListener('pointerup', async function () { + const text = codeBlocks.get(i).innerText; + await copyText(text); + toastr.info(t`Copied!`, '', { timeOut: 2000 }); + }); } } @@ -2724,7 +2724,7 @@ export function getStoppingStrings(isImpersonate, isContinue) { export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null, force_chid = null) { console.log('got into genQuietPrompt'); const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; - let eventHook = () => {}; + let eventHook = () => { }; try { /** @type {GenerateOptions} */ const options = { @@ -3390,7 +3390,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride; const isQuiet = true; - let eventHook = () => {}; + let eventHook = () => { }; if (systemPrompt) { systemPrompt = substituteParams(systemPrompt); @@ -5469,7 +5469,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) { } else { diffPrevPrompt.style.display = 'none'; } - popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('click', function () { + popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () { let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; let rawPromptValues = rawPrompt; @@ -5477,7 +5477,7 @@ async function promptItemize(itemizedPrompts, requestedMesId) { rawPromptValues = rawPrompt.map(x => x.content).join('\n'); } - navigator.clipboard.writeText(rawPromptValues); + await copyText(rawPromptValues); toastr.info(t`Copied!`); }); @@ -9507,7 +9507,7 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api //console.log(logMessage); try { - await navigator.clipboard.writeText(logMessage); + await copyText(logMessage); toastr.info('Your ST API setup data has been copied to the clipboard.'); } catch (error) { toastr.error('Failed to copy ST Setup to clipboard:', error); @@ -10572,24 +10572,18 @@ jQuery(async function () { setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration); }); - if (navigator.clipboard === undefined) { - // No clipboard support - $('.mes_copy').remove(); - } - else { - $(document).on('pointerup', '.mes_copy', function () { - if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) { - try { - const messageId = $(this).closest('.mes').attr('mesid'); - const text = chat[messageId]['mes']; - navigator.clipboard.writeText(text); - toastr.info('Copied!', '', { timeOut: 2000 }); - } catch (err) { - console.error('Failed to copy: ', err); - } + $(document).on('pointerup', '.mes_copy', async function () { + if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) { + try { + const messageId = $(this).closest('.mes').attr('mesid'); + const text = chat[messageId]['mes']; + await copyText(text); + toastr.info('Copied!', '', { timeOut: 2000 }); + } catch (err) { + console.error('Failed to copy: ', err); } - }); - } + } + }); $(document).on('pointerup', '.mes_prompt', async function () { let mesIdForItemization = $(this).closest('.mes').attr('mesId'); @@ -11483,4 +11477,3 @@ jQuery(async function () { initCustomSelectedSamplers(); }); - diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 60b49797a..a479aee52 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -391,6 +391,26 @@ export function getStringHash(str, seed = 0) { return 4294967296 * (2097151 & h2) + (h1 >>> 0); } +/** + * Copy text to clipboard. Use navigator.clipboard.writeText if available, otherwise use document.execCommand. + * @param {string} text - The text to copy to the clipboard. + * @returns {Promise} A promise that resolves when the text has been copied to the clipboard. + */ +export function copyText(text) { + if (navigator.clipboard) { + return navigator.clipboard.writeText(text); + } + + const parent = document.querySelector('dialog[open]:last-of-type') ?? document.body; + const textArea = document.createElement('textarea'); + textArea.value = text; + parent.appendChild(textArea); + textArea.focus(); + textArea.select(); + document.execCommand('copy'); + parent.removeChild(textArea); +} + /** * Map of debounced functions to their timers. * Weak map is used to avoid memory leaks. From a53ebe75723a0aa184e438bab26704bf05cc623b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:47:55 +0200 Subject: [PATCH 12/83] Add vllm.svg --- public/img/vllm.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/img/vllm.svg diff --git a/public/img/vllm.svg b/public/img/vllm.svg new file mode 100644 index 000000000..764d5d4c8 --- /dev/null +++ b/public/img/vllm.svg @@ -0,0 +1 @@ + \ No newline at end of file From 4d18ddba6de251bbe3a59b826a28524c88bf6251 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:18:27 +0200 Subject: [PATCH 13/83] config.yaml: Group extension settings into one section --- default/config.yaml | 32 ++++++++++++++++--------------- jsconfig.json | 2 +- post-install.js | 40 +++++++++++++++++++++++++++++++++++++++ src/endpoints/settings.js | 4 ++-- src/endpoints/vectors.js | 2 +- src/transformers.js | 12 ++++++------ 6 files changed, 67 insertions(+), 25 deletions(-) diff --git a/default/config.yaml b/default/config.yaml index a9d38a4d0..4c28044dc 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -133,24 +133,26 @@ whitelistImportDomains: ## headers: ## User-Agent: "Googlebot/2.1 (+http://www.google.com/bot.html)" requestOverrides: [] -# -- EXTENSIONS CONFIGURATION -- -# Enable UI extensions -enableExtensions: true -# Automatically update extensions when a release version changes -enableExtensionsAutoUpdate: true + +# EXTENSIONS CONFIGURATION +extensions: + # Enable UI extensions + enabled: true + # Automatically update extensions when a release version changes + autoUpdate: true + models: + # Enables automatic model download from HuggingFace + autoDownload: true + # Additional models for extensions. Expects model IDs from HuggingFace model hub in ONNX format + classification: Cohee/distilbert-base-uncased-go-emotions-onnx + captioning: Xenova/vit-gpt2-image-captioning + embedding: Cohee/jina-embeddings-v2-base-en + speechToText: Xenova/whisper-small + textToSpeech: Xenova/speecht5_tts + # Additional model tokenizers can be downloaded on demand. # Disabling will fallback to another locally available tokenizer. enableDownloadableTokenizers: true -# Extension settings -extras: - # Disables automatic model download from HuggingFace - disableAutoDownload: false - # Extra models for plugins. Expects model IDs from HuggingFace model hub in ONNX format - classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx - captioningModel: Xenova/vit-gpt2-image-captioning - embeddingModel: Cohee/jina-embeddings-v2-base-en - speechToTextModel: Xenova/whisper-small - textToSpeechModel: Xenova/speecht5_tts # -- OPENAI CONFIGURATION -- # A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message promptPlaceholder: "[Start a new chat]" diff --git a/jsconfig.json b/jsconfig.json index 042caf675..4130eaedc 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -15,7 +15,7 @@ "**/node_modules/**", "**/dist/**", "**/.git/**", - "public/lib/**", + "public/**", "backups/**", "data/**", "cache/**", diff --git a/post-install.js b/post-install.js index 97745bb15..d22f55719 100644 --- a/post-install.js +++ b/post-install.js @@ -64,6 +64,46 @@ const keyMigrationMap = [ newKey: 'backups.chat.throttleInterval', migrate: (value) => value, }, + { + oldKey: 'enableExtensions', + newKey: 'extensions.enabled', + migrate: (value) => value, + }, + { + oldKey: 'enableExtensionsAutoUpdate', + newKey: 'extensions.autoUpdate', + migrate: (value) => value, + }, + { + oldKey: 'extras.disableAutoDownload', + newKey: 'extensions.models.autoDownload', + migrate: (value) => !value, + }, + { + oldKey: 'extras.classificationModel', + newKey: 'extensions.models.classification', + migrate: (value) => value, + }, + { + oldKey: 'extras.captioningModel', + newKey: 'extensions.models.captioning', + migrate: (value) => value, + }, + { + oldKey: 'extras.embeddingModel', + newKey: 'extensions.models.embedding', + migrate: (value) => value, + }, + { + oldKey: 'extras.speechToTextModel', + newKey: 'extensions.models.speechToText', + migrate: (value) => value, + }, + { + oldKey: 'extras.textToSpeechModel', + newKey: 'extensions.models.textToSpeech', + migrate: (value) => value, + }, ]; /** diff --git a/src/endpoints/settings.js b/src/endpoints/settings.js index 0c4a7cf28..3cc7e3bec 100644 --- a/src/endpoints/settings.js +++ b/src/endpoints/settings.js @@ -10,8 +10,8 @@ import { getConfigValue, generateTimestamp, removeOldBackups } from '../util.js' import { jsonParser } from '../express-common.js'; import { getAllUserHandles, getUserDirectories } from '../users.js'; -const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true); -const ENABLE_EXTENSIONS_AUTO_UPDATE = getConfigValue('enableExtensionsAutoUpdate', true); +const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true); +const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true); const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); // 10 minutes diff --git a/src/endpoints/vectors.js b/src/endpoints/vectors.js index 325684601..b0b9819ac 100644 --- a/src/endpoints/vectors.js +++ b/src/endpoints/vectors.js @@ -164,7 +164,7 @@ function getSourceSettings(source, request) { }; case 'transformers': return { - model: getConfigValue('extras.embeddingModel', ''), + model: getConfigValue('extensions.models.embedding', ''), }; case 'palm': return { diff --git a/src/transformers.js b/src/transformers.js index 3413101a6..c8f8da4f6 100644 --- a/src/transformers.js +++ b/src/transformers.js @@ -19,31 +19,31 @@ const tasks = { 'text-classification': { defaultModel: 'Cohee/distilbert-base-uncased-go-emotions-onnx', pipeline: null, - configField: 'extras.classificationModel', + configField: 'extensions.models.classification', quantized: true, }, 'image-to-text': { defaultModel: 'Xenova/vit-gpt2-image-captioning', pipeline: null, - configField: 'extras.captioningModel', + configField: 'extensions.models.captioning', quantized: true, }, 'feature-extraction': { defaultModel: 'Xenova/all-mpnet-base-v2', pipeline: null, - configField: 'extras.embeddingModel', + configField: 'extensions.models.embedding', quantized: true, }, 'automatic-speech-recognition': { defaultModel: 'Xenova/whisper-small', pipeline: null, - configField: 'extras.speechToTextModel', + configField: 'extensions.models.speechToText', quantized: true, }, 'text-to-speech': { defaultModel: 'Xenova/speecht5_tts', pipeline: null, - configField: 'extras.textToSpeechModel', + configField: 'extensions.models.textToSpeech', quantized: false, }, }; @@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') { const cacheDir = path.join(globalThis.DATA_ROOT, '_cache'); const model = forceModel || getModelForTask(task); - const localOnly = getConfigValue('extras.disableAutoDownload', false); + const localOnly = !getConfigValue('extensions.models.autoDownload', true); console.log('Initializing transformers.js pipeline for task', task, 'with model', model); const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly }); tasks[task].pipeline = instance; From 4fdff9fce2baae51001654ed35091e5c7b23b184 Mon Sep 17 00:00:00 2001 From: Nodude <75137537+NodudeWasTaken@users.noreply.github.com> Date: Sat, 18 Jan 2025 14:07:09 +0100 Subject: [PATCH 14/83] Fix mes examples being undefined if empty --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 331d2b516..9b498ea2f 100644 --- a/public/script.js +++ b/public/script.js @@ -3827,7 +3827,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro * @returns {string[]} Examples array with block heading */ function parseMesExamples(examplesStr) { - if (examplesStr.length === 0 || examplesStr === '') { + if (!examplesStr || examplesStr.length === 0 || examplesStr === '') { return []; } From d87b925488067b51d763984077c89bf26a9c24f9 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 18 Jan 2025 23:34:03 +0200 Subject: [PATCH 15/83] Bump package version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9410c323f..5784a9946 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.12.10", + "version": "1.12.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.12.10", + "version": "1.12.11", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index fc424f438..672ffada5 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.12.10", + "version": "1.12.11", "scripts": { "start": "node server.js", "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", From d7bb92be540d17d28e4f1c0c0bdec95d2525045a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:31:40 +0200 Subject: [PATCH 16/83] deepseek reasoner Closes #3322 --- public/index.html | 5 +-- public/script.js | 37 ++++++++++++++-------- public/scripts/openai.js | 34 +++++++++++++++++--- public/scripts/sse-stream.js | 15 +++++++++ src/endpoints/backends/chat-completions.js | 8 +++-- 5 files changed, 77 insertions(+), 22 deletions(-) diff --git a/public/index.html b/public/index.html index 39f695090..f5b9adf22 100644 --- a/public/index.html +++ b/public/index.html @@ -1977,12 +1977,12 @@ -
+
@@ -3209,6 +3209,7 @@
diff --git a/public/script.js b/public/script.js index 22ab14db2..2bbc88c9e 100644 --- a/public/script.js +++ b/public/script.js @@ -5655,20 +5655,31 @@ function extractMessageFromData(data) { return data; } - switch (main_api) { - case 'kobold': - return data.results[0].text; - case 'koboldhorde': - return data.text; - case 'textgenerationwebui': - return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; - case 'novel': - return data.output; - case 'openai': - return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; - default: - return ''; + function getTextContext() { + switch (main_api) { + case 'kobold': + return data.results[0].text; + case 'koboldhorde': + return data.text; + case 'textgenerationwebui': + return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; + case 'novel': + return data.output; + case 'openai': + return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; + default: + return ''; + } } + + const content = getTextContext(); + + if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) { + const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? ''; + return [thoughts, content].filter(x => x).join('\n\n'); + } + + return content; } /** diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 4ec0c7749..55a9858af 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -2030,6 +2030,16 @@ async function sendOpenAIRequest(type, messages, signal) { // https://api-docs.deepseek.com/api/create-chat-completion if (isDeepSeek) { generate_data.top_p = generate_data.top_p || Number.EPSILON; + + if (generate_data.model.endsWith('-reasoner')) { + delete generate_data.top_p; + delete generate_data.temperature; + delete generate_data.frequency_penalty; + delete generate_data.presence_penalty; + delete generate_data.top_logprobs; + delete generate_data.logprobs; + delete generate_data.logit_bias; + } } if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) { @@ -2085,6 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) { let text = ''; const swipes = []; const toolCalls = []; + const state = {}; while (true) { const { done, value } = await reader.read(); if (done) return; @@ -2095,9 +2106,9 @@ async function sendOpenAIRequest(type, messages, signal) { if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) { const swipeIndex = parsed.choices[0].index - 1; - swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed); + swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state); } else { - text += getStreamingReply(parsed); + text += getStreamingReply(parsed, state); } ToolManager.parseToolCalls(toolCalls, parsed); @@ -2129,14 +2140,27 @@ async function sendOpenAIRequest(type, messages, signal) { } } -function getStreamingReply(data) { +/** + * Extracts the reply from the response data from a chat completions-like source + * @param {object} data Response data from the chat completions-like source + * @param {object} state Additional state to keep track of + * @returns {string} The reply extracted from the response data + */ +function getStreamingReply(data, state) { if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) { return data?.delta?.text || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) { return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || ''; - } else { + } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) { + const hadThoughts = state.hadThoughts; + const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''; + const content = data.choices?.[0]?.delta?.content || ''; + state.hadThoughts = !!thoughts; + const separator = hadThoughts && !thoughts ? '\n\n' : ''; + return [thoughts, separator, content].filter(x => x).join('\n\n'); + } else { return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; } } @@ -4488,7 +4512,7 @@ async function onModelChange() { if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) { if (oai_settings.max_context_unlocked) { $('#openai_max_context').attr('max', unlocked_max); - } else if (oai_settings.deepseek_model == 'deepseek-chat') { + } else if (['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) { $('#openai_max_context').attr('max', max_64k); } else if (oai_settings.deepseek_model == 'deepseek-coder') { $('#openai_max_context').attr('max', max_16k); diff --git a/public/scripts/sse-stream.js b/public/scripts/sse-stream.js index c1c5f1bd6..3921e7d58 100644 --- a/public/scripts/sse-stream.js +++ b/public/scripts/sse-stream.js @@ -220,6 +220,21 @@ async function* parseStreamData(json) { } return; } + else if (typeof json.choices[0].delta.reasoning_content === 'string' && json.choices[0].delta.reasoning_content.length > 0) { + for (let j = 0; j < json.choices[0].delta.reasoning_content.length; j++) { + const str = json.choices[0].delta.reasoning_content[j]; + const isLastSymbol = j === json.choices[0].delta.reasoning_content.length - 1; + const choiceClone = structuredClone(json.choices[0]); + choiceClone.delta.reasoning_content = str; + choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : ''; + const choices = [choiceClone]; + yield { + data: { ...json, choices }, + chunk: str, + }; + } + return; + } else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) { for (let j = 0; j < json.choices[0].delta.content.length; j++) { const str = json.choices[0].delta.content[j]; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 252e16d95..d67f309da 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -61,6 +61,7 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta'; * @returns */ function postProcessPrompt(messages, type, names) { + const addAssistantPrefix = x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x; switch (type) { case 'merge': case 'claude': @@ -70,7 +71,9 @@ function postProcessPrompt(messages, type, names) { case 'strict': return mergeMessages(messages, names, true, true); case 'deepseek': - return (x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x)(mergeMessages(messages, names, true, false)); + return addAssistantPrefix(mergeMessages(messages, names, true, false)); + case 'deepseek-reasoner': + return addAssistantPrefix(mergeMessages(messages, names, true, true)); default: return messages; } @@ -965,7 +968,8 @@ router.post('/generate', jsonParser, function (request, response) { bodyParams['logprobs'] = true; } - request.body.messages = postProcessPrompt(request.body.messages, 'deepseek', getPromptNames(request)); + const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek'; + request.body.messages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request)); } else { console.log('This chat completion source is not supported yet.'); return response.status(400).send({ error: true }); From 08b6ee02979d132955272d00d36f83fd27ec11a8 Mon Sep 17 00:00:00 2001 From: qvink Date: Tue, 21 Jan 2025 16:50:01 -0700 Subject: [PATCH 17/83] exporting openGroupId from group-chats.js --- public/scripts/group-chats.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 635343f33..bc7434993 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -81,6 +81,7 @@ import { t } from './i18n.js'; export { selected_group, + openGroupId, is_group_automode_enabled, hideMutedSprites, is_group_generating, From 7877e6601da89971ba505a6915a349a2d14be658 Mon Sep 17 00:00:00 2001 From: Karl-Johan Alm Date: Wed, 22 Jan 2025 13:13:41 +0900 Subject: [PATCH 18/83] add presets for DeepSeek R1 models --- default/content/index.json | 8 +++++++ .../content/presets/context/DeepSeek-R1.json | 11 ++++++++++ .../content/presets/instruct/DeepSeek-R1.json | 22 +++++++++++++++++++ public/scripts/chat-templates.js | 5 +++++ 4 files changed, 46 insertions(+) create mode 100644 default/content/presets/context/DeepSeek-R1.json create mode 100644 default/content/presets/instruct/DeepSeek-R1.json diff --git a/default/content/index.json b/default/content/index.json index 6df03b78d..bcabafa3d 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -782,5 +782,13 @@ { "filename": "presets/context/Mistral V7.json", "type": "context" + }, + { + "filename": "presets/instruct/DeepSeek-R1.json", + "type": "instruct" + }, + { + "filename": "presets/context/DeepSeek-R1.json", + "type": "context" } ] diff --git a/default/content/presets/context/DeepSeek-R1.json b/default/content/presets/context/DeepSeek-R1.json new file mode 100644 index 000000000..e84c66ced --- /dev/null +++ b/default/content/presets/context/DeepSeek-R1.json @@ -0,0 +1,11 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}\n", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "single_line": false, + "name": "DeepSeek-R1" +} diff --git a/default/content/presets/instruct/DeepSeek-R1.json b/default/content/presets/instruct/DeepSeek-R1.json new file mode 100644 index 000000000..db5e5f907 --- /dev/null +++ b/default/content/presets/instruct/DeepSeek-R1.json @@ -0,0 +1,22 @@ +{ + "input_sequence": "<|User|>", + "output_sequence": "<|Assistant|>", + "first_output_sequence": "", + "last_output_sequence": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "stop_sequence": "", + "wrap": false, + "macro": true, + "names_behavior": "always", + "activation_regex": "", + "skip_examples": false, + "output_suffix": "<|end▁of▁sentence|>", + "input_suffix": "", + "system_sequence": "", + "system_suffix": "", + "user_alignment_message": "", + "last_system_sequence": "", + "system_same_as_user": false, + "name": "DeepSeek-R1" +} diff --git a/public/scripts/chat-templates.js b/public/scripts/chat-templates.js index ea732b90c..19baa9d11 100644 --- a/public/scripts/chat-templates.js +++ b/public/scripts/chat-templates.js @@ -59,6 +59,11 @@ const hash_derivations = { // Tulu-3-8B // Tulu-3-70B 'Tulu' + , + + // DeepSeek R1 + 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1': + 'DeepSeek-R1' }; const substr_derivations = { From 1641b1f91f015481d4186dca1c66df370969766d Mon Sep 17 00:00:00 2001 From: qvink Date: Tue, 21 Jan 2025 22:14:35 -0700 Subject: [PATCH 19/83] adding event after loading more messages in chat --- public/script.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/script.js b/public/script.js index 2bbc88c9e..3e92c154b 100644 --- a/public/script.js +++ b/public/script.js @@ -443,6 +443,7 @@ export const event_types = { MESSAGE_DELETED: 'message_deleted', MESSAGE_UPDATED: 'message_updated', MESSAGE_FILE_EMBEDDED: 'message_file_embedded', + MORE_MESSAGES_LOADED: 'more_messages_loaded', IMPERSONATE_READY: 'impersonate_ready', CHAT_CHANGED: 'chat_id_changed', GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS', @@ -1859,6 +1860,8 @@ export function showMoreMessages(messagesToLoad = null) { const newHeight = $('#chat').prop('scrollHeight'); $('#chat').scrollTop(newHeight - prevHeight); } + + eventSource.emit(event_types.MORE_MESSAGES_LOADED) } export async function printMessages() { From 636e79c4388cf925ad1a3df2f2b71735d36b02ab Mon Sep 17 00:00:00 2001 From: Karl-Johan Alm Date: Wed, 22 Jan 2025 16:03:03 +0900 Subject: [PATCH 20/83] fixes --- default/content/index.json | 4 ++-- .../presets/context/{DeepSeek-R1.json => DeepSeek-V2.5.json} | 2 +- .../presets/instruct/{DeepSeek-R1.json => DeepSeek-V2.5.json} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename default/content/presets/context/{DeepSeek-R1.json => DeepSeek-V2.5.json} (95%) rename default/content/presets/instruct/{DeepSeek-R1.json => DeepSeek-V2.5.json} (90%) diff --git a/default/content/index.json b/default/content/index.json index bcabafa3d..82387d0a9 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -784,11 +784,11 @@ "type": "context" }, { - "filename": "presets/instruct/DeepSeek-R1.json", + "filename": "presets/instruct/DeepSeek-V2.5.json", "type": "instruct" }, { - "filename": "presets/context/DeepSeek-R1.json", + "filename": "presets/context/DeepSeek-V2.5.json", "type": "context" } ] diff --git a/default/content/presets/context/DeepSeek-R1.json b/default/content/presets/context/DeepSeek-V2.5.json similarity index 95% rename from default/content/presets/context/DeepSeek-R1.json rename to default/content/presets/context/DeepSeek-V2.5.json index e84c66ced..49efaba59 100644 --- a/default/content/presets/context/DeepSeek-R1.json +++ b/default/content/presets/context/DeepSeek-V2.5.json @@ -7,5 +7,5 @@ "always_force_name2": true, "trim_sentences": false, "single_line": false, - "name": "DeepSeek-R1" + "name": "DeepSeek-V2.5" } diff --git a/default/content/presets/instruct/DeepSeek-R1.json b/default/content/presets/instruct/DeepSeek-V2.5.json similarity index 90% rename from default/content/presets/instruct/DeepSeek-R1.json rename to default/content/presets/instruct/DeepSeek-V2.5.json index db5e5f907..63fb45362 100644 --- a/default/content/presets/instruct/DeepSeek-R1.json +++ b/default/content/presets/instruct/DeepSeek-V2.5.json @@ -8,7 +8,7 @@ "stop_sequence": "", "wrap": false, "macro": true, - "names_behavior": "always", + "names_behavior": "force", "activation_regex": "", "skip_examples": false, "output_suffix": "<|end▁of▁sentence|>", @@ -18,5 +18,5 @@ "user_alignment_message": "", "last_system_sequence": "", "system_same_as_user": false, - "name": "DeepSeek-R1" + "name": "DeepSeek-V2.5" } From 2b00cdce7b6fdd282ef47f58753fc3329b1bdd00 Mon Sep 17 00:00:00 2001 From: Karl-Johan Alm Date: Wed, 22 Jan 2025 17:07:23 +0900 Subject: [PATCH 21/83] system same as user --- default/content/presets/instruct/DeepSeek-V2.5.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default/content/presets/instruct/DeepSeek-V2.5.json b/default/content/presets/instruct/DeepSeek-V2.5.json index 63fb45362..7990d13c0 100644 --- a/default/content/presets/instruct/DeepSeek-V2.5.json +++ b/default/content/presets/instruct/DeepSeek-V2.5.json @@ -17,6 +17,6 @@ "system_suffix": "", "user_alignment_message": "", "last_system_sequence": "", - "system_same_as_user": false, + "system_same_as_user": true, "name": "DeepSeek-V2.5" } From 11882827c789595f646a519fbfcd60bc0cc6abc8 Mon Sep 17 00:00:00 2001 From: sirius422 Date: Wed, 22 Jan 2025 18:37:43 +0800 Subject: [PATCH 22/83] Add new Gemini thinking model and its alias, specify context size and vision support --- public/index.html | 4 +++- public/scripts/extensions/caption/settings.html | 2 ++ public/scripts/openai.js | 2 +- src/prompt-converters.js | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index f5b9adf22..3984afa5e 100644 --- a/public/index.html +++ b/public/index.html @@ -3062,7 +3062,9 @@ - + + + diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index 187e47876..3f344da0c 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -54,6 +54,8 @@ + + diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 55a9858af..b1a3ded9d 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4228,7 +4228,7 @@ async function onModelChange() { $('#openai_max_context').attr('max', max_32k); } else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) { $('#openai_max_context').attr('max', max_2mil); - } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) { + } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) { $('#openai_max_context').attr('max', max_1mil); } else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') { $('#openai_max_context').attr('max', max_32k); diff --git a/src/prompt-converters.js b/src/prompt-converters.js index e38813c99..49afc1f2f 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -360,6 +360,8 @@ export function convertCohereMessages(messages, names) { */ export function convertGooglePrompt(messages, model, useSysPrompt, names) { const visionSupportedModels = [ + 'gemini-2.0-flash-thinking-exp', + 'gemini-2.0-flash-thinking-exp-01-21', 'gemini-2.0-flash-thinking-exp-1219', 'gemini-2.0-flash-exp', 'gemini-1.5-flash', From 93b18e6440ecf378a692e62a766db9d87c6fb04d Mon Sep 17 00:00:00 2001 From: Karl-Johan Alm Date: Wed, 22 Jan 2025 21:34:05 +0900 Subject: [PATCH 23/83] fix: corrects the preset name and adds deepseek v2.5 hash. --- public/scripts/chat-templates.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/chat-templates.js b/public/scripts/chat-templates.js index 19baa9d11..98f1ee310 100644 --- a/public/scripts/chat-templates.js +++ b/public/scripts/chat-templates.js @@ -61,9 +61,14 @@ const hash_derivations = { 'Tulu' , + // DeepSeek V2.5 + '54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca': + 'DeepSeek-V2.5' + , + // DeepSeek R1 'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1': - 'DeepSeek-R1' + 'DeepSeek-V2.5' }; const substr_derivations = { From f1923c5364d46fd6501e3c43e8697a030d22ef99 Mon Sep 17 00:00:00 2001 From: qvink Date: Wed, 22 Jan 2025 08:36:47 -0700 Subject: [PATCH 24/83] making showMoreMessages async to await event emission --- public/script.js | 8 ++++---- public/scripts/power-user.js | 2 +- public/scripts/slash-commands.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index 3e92c154b..f2e84f21d 100644 --- a/public/script.js +++ b/public/script.js @@ -1830,7 +1830,7 @@ export async function replaceCurrentChat() { } } -export function showMoreMessages(messagesToLoad = null) { +export async function showMoreMessages(messagesToLoad = null) { const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); let messageId = Number(firstDisplayedMesId); let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; @@ -1861,7 +1861,7 @@ export function showMoreMessages(messagesToLoad = null) { $('#chat').scrollTop(newHeight - prevHeight); } - eventSource.emit(event_types.MORE_MESSAGES_LOADED) + await eventSource.emit(event_types.MORE_MESSAGES_LOADED) } export async function printMessages() { @@ -11468,8 +11468,8 @@ jQuery(async function () { $('#avatar-and-name-block').slideToggle(); }); - $(document).on('mouseup touchend', '#show_more_messages', () => { - showMoreMessages(); + $(document).on('mouseup touchend', '#show_more_messages', async function () { + await showMoreMessages(); }); $(document).on('click', '.open_characters_library', async function () { diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index b3b7f41d5..bfe8eca2e 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -2534,7 +2534,7 @@ async function loadUntilMesId(mesId) { let target; while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) { - showMoreMessages(); + await showMoreMessages(); await delay(1); target = $('#chat').find(`.mes[mesid=${mesId}]`); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 33725bf2d..39b3db71e 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1968,8 +1968,8 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'chat-render', helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.', - callback: (args, number) => { - showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER); + callback: async (args, number) => { + await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER); if (isTrueBoolean(String(args?.scroll ?? ''))) { $('#chat').scrollTop(0); } From 8f18d351098425cdb437e62df8e041200c1c2aeb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:18:34 +0000 Subject: [PATCH 25/83] [chore] Add missing semi --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index f2e84f21d..3ba3aacbc 100644 --- a/public/script.js +++ b/public/script.js @@ -1861,7 +1861,7 @@ export async function showMoreMessages(messagesToLoad = null) { $('#chat').scrollTop(newHeight - prevHeight); } - await eventSource.emit(event_types.MORE_MESSAGES_LOADED) + await eventSource.emit(event_types.MORE_MESSAGES_LOADED); } export async function printMessages() { From ae29f06e44a955fcbbca17003a32e2cfcea2210c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:38:13 +0200 Subject: [PATCH 26/83] Gemini: Fix image inlining for new models #3332 --- public/scripts/openai.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index b1a3ded9d..54e9f6125 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4925,6 +4925,8 @@ export function isImageInliningSupported() { const visionSupportedModels = [ 'gpt-4-vision', 'gemini-2.0-flash-thinking-exp-1219', + 'gemini-2.0-flash-thinking-exp-01-21', + 'gemini-2.0-flash-thinking-exp', 'gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-flash-latest', From 7c93acedc33555077914de40696ec45a545510d2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:41:34 +0200 Subject: [PATCH 27/83] post-install: Fix getting keys of null values Original commit by @honey-tree https://github.com/SillyTavern/SillyTavern/pull/3334/commits/5c40876a4afb928b0b0ea916a7f02ee57234e452 --- post-install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/post-install.js b/post-install.js index 97745bb15..63c3de926 100644 --- a/post-install.js +++ b/post-install.js @@ -73,7 +73,7 @@ const keyMigrationMap = [ * @returns {string[]} Array of all keys in the object */ function getAllKeys(obj, prefix = '') { - if (typeof obj !== 'object' || Array.isArray(obj)) { + if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) { return []; } From e4290140bc4b8af3652f847ed7704729f0403118 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:45:30 +0200 Subject: [PATCH 28/83] TC: Remove -1 seeds from request body Closes #3336 --- public/scripts/textgen-settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index e80c7e910..cd1004991 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -1231,7 +1231,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'top_p': settings.top_p, 'typical_p': settings.typical_p, 'typical': settings.typical_p, - 'sampler_seed': settings.seed, + 'sampler_seed': settings.seed >= 0 ? settings.seed : undefined, 'min_p': settings.min_p, 'repetition_penalty': settings.rep_pen, 'frequency_penalty': settings.freq_pen, @@ -1294,7 +1294,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined, 'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined, 'do_sample': settings.type === OOBA ? settings.do_sample : undefined, - 'seed': settings.seed, + 'seed': settings.seed >= 0 ? settings.seed : undefined, 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1, 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '', 'grammar_string': settings.grammar_string, From 6fef6962689b94d2f9b6c58f8fa6641618c8ecf3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:58:58 +0200 Subject: [PATCH 29/83] Featherless: use scaleable font sizes for models list --- public/style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/style.css b/public/style.css index f5d8da36d..cbfc96185 100644 --- a/public/style.css +++ b/public/style.css @@ -5643,6 +5643,7 @@ body:not(.movingUI) .drawer-content.maximized { .model-card .details-container { text-align: right; + line-height: 0.9; } .model-card:hover { @@ -5665,7 +5666,7 @@ body:not(.movingUI) .drawer-content.maximized { } .model-title { - font-size: 13px; + font-size: calc(var(--mainFontSize) * 0.95); font-weight: bold; overflow: hidden; } @@ -5681,7 +5682,7 @@ body:not(.movingUI) .drawer-content.maximized { .model-class, .model-context-length, .model-date-added { - font-size: 10px; + font-size: calc(var(--mainFontSize) * 0.75); } .model-class, From afae8d02be1e837b1dc5abc013fa011ab4c3d067 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Jan 2025 02:52:52 +0200 Subject: [PATCH 30/83] The THONKening --- public/index.html | 1 + public/script.js | 111 +++++++++++++++------ public/scripts/constants.js | 5 + public/scripts/kai-settings.js | 2 +- public/scripts/nai-settings.js | 2 +- public/scripts/openai.js | 19 ++-- public/scripts/textgen-settings.js | 3 +- public/style.css | 28 +++++- src/constants.js | 5 + src/endpoints/backends/chat-completions.js | 3 +- 10 files changed, 130 insertions(+), 49 deletions(-) diff --git a/public/index.html b/public/index.html index 3984afa5e..3fe2162fb 100644 --- a/public/index.html +++ b/public/index.html @@ -6229,6 +6229,7 @@
+
diff --git a/public/script.js b/public/script.js index 3ba3aacbc..b641e161c 100644 --- a/public/script.js +++ b/public/script.js @@ -170,7 +170,7 @@ import { isElementInViewport, copyText, } from './scripts/utils.js'; -import { debounce_timeout } from './scripts/constants.js'; +import { debounce_timeout, THINK_BREAK } from './scripts/constants.js'; import { doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js'; import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js'; @@ -2199,6 +2199,7 @@ function getMessageFromTemplate({ isUser, avatarImg, bias, + reasoning, isSystem, title, timerValue, @@ -2223,6 +2224,7 @@ function getMessageFromTemplate({ mes.find('.avatar img').attr('src', avatarImg); mes.find('.ch_name .name_text').text(characterName); mes.find('.mes_bias').html(bias); + mes.find('.mes_reasoning').html(reasoning); mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`); mes.find('.mesIDDisplay').text(`#${mesId}`); tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`); @@ -2241,6 +2243,7 @@ export function updateMessageBlock(messageId, message) { const messageElement = $(`#chat [mesid="${messageId}"]`); const text = message?.extra?.display_text ?? message.mes; messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId)); + messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1)); addCopyToCodeBlocks(messageElement); appendMediaToMessage(message, messageElement); } @@ -2399,6 +2402,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll sanitizerOverrides, ); const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1); + const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1); let bookmarkLink = mes?.extra?.bookmark_link ?? ''; let params = { @@ -2408,6 +2412,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll isUser: mes.is_user, avatarImg: avatarImg, bias: bias, + reasoning: reasoning, isSystem: isSystem, title: title, bookmarkLink: bookmarkLink, @@ -2467,6 +2472,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`); swipeMessage.attr('swipeid', params.swipeId); swipeMessage.find('.mes_text').html(messageText).attr('title', title); + swipeMessage.find('.mes_reasoning').html(reasoning); swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); appendMediaToMessage(mes, swipeMessage); if (power_user.timestamp_model_icon && params.extra?.api) { @@ -3077,6 +3083,7 @@ class StreamingProcessor { this.messageTextDom = null; this.messageTimerDom = null; this.messageTokenCounterDom = null; + this.messageReasoningDom = null; /** @type {HTMLTextAreaElement} */ this.sendTextarea = document.querySelector('#send_textarea'); this.type = type; @@ -3092,6 +3099,7 @@ class StreamingProcessor { /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ this.messageLogprobs = []; this.toolCalls = []; + this.reasoning = ''; } #checkDomElements(messageId) { @@ -3100,6 +3108,7 @@ class StreamingProcessor { this.messageTextDom = this.messageDom?.querySelector('.mes_text'); this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); + this.messageReasoningDom = this.messageDom?.querySelector('.mes_reasoning'); } } @@ -3184,11 +3193,17 @@ class StreamingProcessor { chat[messageId]['gen_started'] = this.timeStarted; chat[messageId]['gen_finished'] = currentTime; - if (currentTokenCount) { - if (!chat[messageId]['extra']) { - chat[messageId]['extra'] = {}; - } + if (!chat[messageId]['extra']) { + chat[messageId]['extra'] = {}; + } + if (this.reasoning && this.messageReasoningDom instanceof HTMLElement) { + chat[messageId]['extra']['reasoning'] = this.reasoning; + const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1); + this.messageReasoningDom.innerHTML = formattedReasoning; + } + + if (currentTokenCount) { chat[messageId]['extra']['token_count'] = currentTokenCount; if (this.messageTokenCounterDom instanceof HTMLElement) { this.messageTokenCounterDom.textContent = `${currentTokenCount}t`; @@ -3320,7 +3335,7 @@ class StreamingProcessor { } /** - * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>} + * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[], state: any }, void, void>} */ *nullStreamingGeneration() { throw new Error('Generation function for streaming is not hooked up'); @@ -3342,7 +3357,7 @@ class StreamingProcessor { try { const sw = new Stopwatch(1000 / power_user.streaming_fps); const timestamps = []; - for await (const { text, swipes, logprobs, toolCalls } of this.generator()) { + for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) { timestamps.push(Date.now()); if (this.isStopped) { return; @@ -3354,6 +3369,7 @@ class StreamingProcessor { if (logprobs) { this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); } + this.reasoning = state?.reasoning ?? ''; await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text)); } @@ -4741,6 +4757,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro //const getData = await response.json(); let getMessage = extractMessageFromData(data); let title = extractTitleFromData(data); + let reasoning = extractReasoningFromData(data); kobold_horde_model = title; const swipes = extractMultiSwipes(data, type); @@ -4767,10 +4784,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro else { // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. if (originalType !== 'continue') { - ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes)); + ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes, reasoning)); } else { - ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes)); + ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes, reasoning)); } // This relies on `saveReply` having been called to add the message to the chat, so it must be last. @@ -5649,42 +5666,65 @@ function parseAndSaveLogprobs(data, continueFrom) { } /** - * Extracts the message from the response data. - * @param {object} data Response data - * @returns {string} Extracted message + * Gets the text context from the response data. + * @param {object} data Response JSON data + * @returns {string} Extracted text */ -function extractMessageFromData(data) { +function getTextContextFromData(data) { if (typeof data === 'string') { return data; } - function getTextContext() { - switch (main_api) { - case 'kobold': - return data.results[0].text; - case 'koboldhorde': - return data.text; - case 'textgenerationwebui': - return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; - case 'novel': - return data.output; - case 'openai': - return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; - default: - return ''; - } + switch (main_api) { + case 'kobold': + return data.results[0].text; + case 'koboldhorde': + return data.text; + case 'textgenerationwebui': + return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; + case 'novel': + return data.output; + case 'openai': + return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; + default: + return ''; } +} - const content = getTextContext(); +/** + * Extracts the message from the response data. + * @param {object} data Response data + * @returns {string} Extracted message + */ +function extractMessageFromData(data){ + const content = String(getTextContextFromData(data) ?? ''); - if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) { - const thoughts = data?.choices?.[0]?.message?.reasoning_content ?? ''; - return [thoughts, content].filter(x => x).join('\n\n'); + if (content.includes(THINK_BREAK)) { + return content.split(THINK_BREAK)[1]; } return content; } +/** + * Extracts the reasoning from the response data. + * @param {object} data Response data + * @returns {string} Extracted reasoning + */ +function extractReasoningFromData(data) { + const content = String(getTextContextFromData(data) ?? ''); + + if (content.includes(THINK_BREAK)) { + return content.split(THINK_BREAK)[0]; + } + + if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) { + return data?.choices?.[0]?.message?.reasoning_content ?? ''; + } + + return ''; +} + /** * Extracts multiswipe swipes from the response data. * @param {Object} data Response data @@ -5865,7 +5905,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc return getMessage; } -export async function saveReply(type, getMessage, fromStreaming, title, swipes) { +export async function saveReply(type, getMessage, fromStreaming, title, swipes, reasoning) { if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || chat[chat.length - 1]['is_user'])) { type = 'normal'; @@ -5890,6 +5930,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] = reasoning; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } @@ -5910,6 +5951,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } @@ -5927,6 +5969,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } @@ -5944,6 +5987,7 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes) chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); + chat[chat.length - 1]['extra']['reasoning'] = reasoning; if (power_user.trim_spaces) { getMessage = getMessage.trim(); } @@ -8646,6 +8690,7 @@ const swipe_right = () => { // resets the timer swipeMessage.find('.mes_timer').html(''); swipeMessage.find('.tokenCounterDisplay').text(''); + swipeMessage.find('.mes_reasoning').html(''); } else { //console.log('showing previously generated swipe candidate, or "..."'); //console.log('onclick right swipe calling addOneMessage'); diff --git a/public/scripts/constants.js b/public/scripts/constants.js index f95a8e146..935a74219 100644 --- a/public/scripts/constants.js +++ b/public/scripts/constants.js @@ -14,3 +14,8 @@ export const debounce_timeout = { /** [5 sec] For delayed tasks, like auto-saving or completing batch operations that need a significant pause. */ extended: 5000, }; + +/** + * Custom boundary for splitting the text between the model's reasoning and the actual response. + */ +export const THINK_BREAK = '##�THINK_BREAK�##'; diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index 6efadce87..65d47fc4b 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) { if (data?.token) { text += data.token; } - yield { text, swipes: [], toolCalls: [] }; + yield { text, swipes: [], toolCalls: [], state: {} }; } }; } diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js index f95e7d9f6..91ff09ef6 100644 --- a/public/scripts/nai-settings.js +++ b/public/scripts/nai-settings.js @@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) { text += data.token; } - yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] }; + yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [], state: {} }; } }; } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 54e9f6125..5585f82ef 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -2095,7 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) { let text = ''; const swipes = []; const toolCalls = []; - const state = {}; + const state = { reasoning: '' }; while (true) { const { done, value } = await reader.read(); if (done) return; @@ -2113,7 +2113,7 @@ async function sendOpenAIRequest(type, messages, signal) { ToolManager.parseToolCalls(toolCalls, parsed); - yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls }; + yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls, state: state }; } }; } @@ -2150,16 +2150,17 @@ function getStreamingReply(data, state) { if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) { return data?.delta?.text || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { - return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || ''; + if (oai_settings.show_thoughts) { + state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || ''); + } + return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) { return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || ''; } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) { - const hadThoughts = state.hadThoughts; - const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''; - const content = data.choices?.[0]?.delta?.content || ''; - state.hadThoughts = !!thoughts; - const separator = hadThoughts && !thoughts ? '\n\n' : ''; - return [thoughts, separator, content].filter(x => x).join('\n\n'); + if (oai_settings.show_thoughts) { + state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''); + } + return data.choices?.[0]?.delta?.content || ''; } else { return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; } diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index cd1004991..19b729374 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -986,6 +986,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) { let logprobs = null; const swipes = []; const toolCalls = []; + const state = {}; while (true) { const { done, value } = await reader.read(); if (done) return; @@ -1004,7 +1005,7 @@ export async function generateTextGenWithStreaming(generate_data, signal) { logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities); } - yield { text, swipes, logprobs, toolCalls }; + yield { text, swipes, logprobs, toolCalls, state }; } }; } diff --git a/public/style.css b/public/style.css index cbfc96185..ee7cf9094 100644 --- a/public/style.css +++ b/public/style.css @@ -332,6 +332,23 @@ input[type='checkbox']:focus-visible { color: var(--SmartThemeQuoteColor); } +.mes_reasoning { + display: block; + border: 1px solid var(--SmartThemeBorderColor); + background-color: var(--black30a); + border-radius: 5px; + padding: 5px; + margin: 5px 0; + overflow-y: auto; + max-height: 100px; +} + +.mes_block:has(.edit_textarea) .mes_reasoning, +.mes_bias:empty, +.mes_reasoning:empty { + display: none; +} + .mes_text i, .mes_text em { color: var(--SmartThemeEmColor); @@ -1022,6 +1039,7 @@ body .panelControlBar { /*only affects bubblechat to make it sit nicely at the bottom*/ } +.last_mes .mes_reasoning, .last_mes .mes_text { padding-right: 30px; } @@ -1235,14 +1253,18 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { overflow-y: clip; } -.mes_text { +.mes_text, +.mes_reasoning { font-weight: 500; line-height: calc(var(--mainFontSize) + .5rem); + max-width: 100%; + overflow-wrap: anywhere; +} + +.mes_text { padding-left: 0; padding-top: 5px; padding-bottom: 5px; - max-width: 100%; - overflow-wrap: anywhere; } br { diff --git a/src/constants.js b/src/constants.js index 35118a04b..faddaaf81 100644 --- a/src/constants.js +++ b/src/constants.js @@ -413,3 +413,8 @@ export const VLLM_KEYS = [ 'guided_decoding_backend', 'guided_whitespace_pattern', ]; + +/** + * Custom boundary for splitting the text between the model's reasoning and the actual response. + */ +export const THINK_BREAK = '##�THINK_BREAK�##'; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index d67f309da..5e81b2280 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -7,6 +7,7 @@ import { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, OPENROUTER_HEADERS, + THINK_BREAK, } from '../../constants.js'; import { forwardFetchResponse, @@ -389,7 +390,7 @@ async function sendMakerSuiteRequest(request, response) { responseContent.parts = responseContent.parts.filter(part => !part.thought); } - const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join('\n\n'); + const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join(THINK_BREAK); if (!responseText) { let message = 'Google AI Studio Candidate text empty'; console.log(message, generateResponseJson); From a503f58d0cbbd32477cc415de7fbfa98512df7b5 Mon Sep 17 00:00:00 2001 From: subzero5544 Date: Thu, 23 Jan 2025 01:02:44 -0600 Subject: [PATCH 31/83] Adding reverse proxy support to DeepSeek chat completion (#3328) * added reverse proxy settings to deepseek chat completion * Update chat-completions.js * Update chat-completions.js * Update chat-completions.js * Update chat-completions.js * Update chat-completions.js * Update chat-completions.js * Unify API key requirement --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> --- public/index.html | 4 +- public/scripts/openai.js | 6 +- src/endpoints/backends/chat-completions.js | 102 +++++++++++++++++---- 3 files changed, 91 insertions(+), 21 deletions(-) diff --git a/public/index.html b/public/index.html index 3984afa5e..f26c95aa0 100644 --- a/public/index.html +++ b/public/index.html @@ -2692,7 +2692,7 @@ -
+
Reverse Proxy
@@ -2755,7 +2755,7 @@
-
+
diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 54e9f6125..d715377fb 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1922,7 +1922,7 @@ async function sendOpenAIRequest(type, messages, signal) { } // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite - if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) { + if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) { await validateReverseProxy(); generate_data['reverse_proxy'] = oai_settings.reverse_proxy; generate_data['proxy_password'] = oai_settings.proxy_password; @@ -3370,7 +3370,7 @@ async function getStatusOpen() { chat_completion_source: oai_settings.chat_completion_source, }; - if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) { + if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) { await validateReverseProxy(); } @@ -4749,7 +4749,7 @@ async function onConnectButtonClick(e) { await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek); } - if (!secret_state[SECRET_KEYS.DEEPSEEK]) { + if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) { console.log('No secret key saved for DeepSeek'); return; } diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index d67f309da..9dcaf4fcf 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -639,6 +639,89 @@ async function sendCohereRequest(request, response) { } } +/** + * Sends a request to DeepSeek API. + * @param {express.Request} request Express request + * @param {express.Response} response Express response + */ +async function sendDeepSeekRequest(request, response) { + const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString(); + const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK); + + if (!apiKey && !request.body.reverse_proxy) { + console.log('DeepSeek API key is missing.'); + return response.status(400).send({ error: true }); + } + + const controller = new AbortController(); + request.socket.removeAllListeners('close'); + request.socket.on('close', function () { + controller.abort(); + }); + + try { + let bodyParams = {}; + + if (request.body.logprobs > 0) { + bodyParams['top_logprobs'] = request.body.logprobs; + bodyParams['logprobs'] = true; + } + + const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek'; + const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request)); + + const requestBody = { + 'messages': processedMessages, + 'model': request.body.model, + 'temperature': request.body.temperature, + 'max_tokens': request.body.max_tokens, + 'stream': request.body.stream, + 'presence_penalty': request.body.presence_penalty, + 'frequency_penalty': request.body.frequency_penalty, + 'top_p': request.body.top_p, + 'stop': request.body.stop, + 'seed': request.body.seed, + ...bodyParams, + }; + + const config = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + apiKey, + }, + body: JSON.stringify(requestBody), + signal: controller.signal, + }; + + console.log('DeepSeek request:', requestBody); + + const generateResponse = await fetch(apiUrl + '/chat/completions', config); + + if (request.body.stream) { + forwardFetchResponse(generateResponse, response); + } else { + if (!generateResponse.ok) { + const errorText = await generateResponse.text(); + console.log(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`); + const errorJson = tryParse(errorText) ?? { error: true }; + return response.status(500).send(errorJson); + } + const generateResponseJson = await generateResponse.json(); + console.log('DeepSeek response:', generateResponseJson); + return response.send(generateResponseJson); + } + } catch (error) { + console.log('Error communicating with DeepSeek API: ', error); + if (!response.headersSent) { + response.send({ error: true }); + } else { + response.end(); + } + } +} + + export const router = express.Router(); router.post('/status', jsonParser, async function (request, response_getstatus_openai) { @@ -683,8 +766,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o api_key_openai = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); headers = {}; } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) { - api_url = API_DEEPSEEK.replace('/beta', ''); - api_key_openai = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK); + api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', '')); + api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK); headers = {}; } else { console.log('This chat completion source is not supported yet.'); @@ -844,6 +927,7 @@ router.post('/generate', jsonParser, function (request, response) { case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response); case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response); case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response); + case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response); } let apiUrl; @@ -957,19 +1041,6 @@ router.post('/generate', jsonParser, function (request, response) { apiKey = readSecret(request.user.directories, SECRET_KEYS.BLOCKENTROPY); headers = {}; bodyParams = {}; - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) { - apiUrl = API_DEEPSEEK; - apiKey = readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK); - headers = {}; - bodyParams = {}; - - if (request.body.logprobs > 0) { - bodyParams['top_logprobs'] = request.body.logprobs; - bodyParams['logprobs'] = true; - } - - const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek'; - request.body.messages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request)); } else { console.log('This chat completion source is not supported yet.'); return response.status(400).send({ error: true }); @@ -1107,4 +1178,3 @@ router.post('/generate', jsonParser, function (request, response) { } } }); - From 71be63dbb1ecd605fbda7b614f00d2bfe4f6bc3b Mon Sep 17 00:00:00 2001 From: Sevenyine <65720409+Sevenyine@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:23:46 +0800 Subject: [PATCH 32/83] Update zh-CN translations about lorebook --- public/locales/zh-cn.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index bf45663d6..82a9a6c6d 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -2045,8 +2045,8 @@ "Post a GitHub issue": "在 GitHub 发布问题", "Contact the developers": "联系开发者", "If you're connected to an API, try asking me something!": "若您已经配置好API,尝试发送些什么吧!", - "Title/Memo": "标题/备忘录", - "Strategy": "Strategy", - "Position": "位置", - "Trigger %": "触发率 %" + "Title/Memo": "标题(备忘)", + "Strategy": "触发策略", + "Position": "插入位置", + "Trigger %": "触发率%" } From 515f78619f2184965cc8e9c3dab51af9275ab820 Mon Sep 17 00:00:00 2001 From: Sevenyine <65720409+Sevenyine@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:25:17 +0800 Subject: [PATCH 33/83] Update --- public/locales/zh-cn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 82a9a6c6d..267da34c8 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -2048,5 +2048,5 @@ "Title/Memo": "标题(备忘)", "Strategy": "触发策略", "Position": "插入位置", - "Trigger %": "触发率%" + "Trigger %": "触发概率%" } From 8b2d97b9465f9ad100a6096c9e98f29aac67546c Mon Sep 17 00:00:00 2001 From: Sevenyine <65720409+Sevenyine@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:32:41 +0800 Subject: [PATCH 34/83] Modification --- public/locales/zh-cn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 267da34c8..56e8c1d64 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1838,7 +1838,7 @@ "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装", "Disclaimer:": "免责声明:", "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.": "使用外部的扩展程序可能存在意料外的副作用和安全隐患。在导入扩展程序前,请一定确认其来源可信。我们不为第三方扩展程序造成的任何损失负责。", - "Prompt Itemization": "将提示词分条", + "Prompt Itemization": "提示词拆分", "Show Raw Prompt": "显示原始提示词", "Copy Prompt": "复制提示词", "Show Prompt Differences": "显示提示词差异", From adad1fde1941c0ba18a3114535635063aeeeb45e Mon Sep 17 00:00:00 2001 From: Sevenyine <65720409+Sevenyine@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:56:59 +0800 Subject: [PATCH 35/83] Modified Translations about Group Chats --- public/locales/zh-cn.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 56e8c1d64..04129749c 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1191,9 +1191,9 @@ "welcome_message_part_8": "您可随时通过", "welcome_message_part_9": "图标来更改此设置。", "Persona Name:": "用户角色名称:", - "Temporarily disable automatic replies from this character": "暂时禁用此角色的自动回复", - "Enable automatic replies from this character": "启用此角色的自动回复", - "Trigger a message from this character": "从此角色触发消息", + "Temporarily disable automatic replies from this character": "临时禁言此角色", + "Enable automatic replies from this character": "解除禁言此角色", + "Trigger a message from this character": "强制触发该角色发言", "Move up": "向上移动", "Move down": "向下移动", "View character card": "查看角色卡片", From 823b9db6f6d6403354433584ecd02f14c603de68 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:41:39 +0200 Subject: [PATCH 36/83] Gemini: Fix requesting thought blocks --- src/endpoints/backends/chat-completions.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 5e81b2280..f50a35240 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -288,6 +288,7 @@ async function sendMakerSuiteRequest(request, response) { const model = String(request.body.model); const stream = Boolean(request.body.stream); const showThoughts = Boolean(request.body.show_thoughts); + const isThinking = model.includes('thinking'); const generationConfig = { stopSequences: request.body.stop, @@ -328,6 +329,12 @@ async function sendMakerSuiteRequest(request, response) { body.systemInstruction = prompt.system_instruction; } + if (isThinking && showThoughts) { + generationConfig.thinkingConfig = { + includeThoughts: true, + }; + } + return body; } @@ -341,7 +348,6 @@ async function sendMakerSuiteRequest(request, response) { controller.abort(); }); - const isThinking = model.includes('thinking'); const apiVersion = isThinking ? 'v1alpha' : 'v1beta'; const responseType = (stream ? 'streamGenerateContent' : 'generateContent'); From 144277bdcc5ac110d888510b79d0ad6a8d92873a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:43:04 +0200 Subject: [PATCH 37/83] Wrap thonk into collapsible --- public/index.html | 7 ++++++- public/style.css | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 3fe2162fb..4255bcae5 100644 --- a/public/index.html +++ b/public/index.html @@ -6229,7 +6229,12 @@
-
+
+ + Reasoning + +
+
diff --git a/public/style.css b/public/style.css index ee7cf9094..d0c528f5a 100644 --- a/public/style.css +++ b/public/style.css @@ -340,10 +340,19 @@ input[type='checkbox']:focus-visible { padding: 5px; margin: 5px 0; overflow-y: auto; - max-height: 100px; + max-height: min(50vh, 300px); } -.mes_block:has(.edit_textarea) .mes_reasoning, +.mes_reasoning_summary { + cursor: pointer; +} + +.mes_reasoning_summary > span { + margin-left: 5px; +} + +.mes_reasoning_details:has(.mes_reasoning:empty), +.mes_block:has(.edit_textarea) .mes_reasoning_details, .mes_bias:empty, .mes_reasoning:empty { display: none; From 6aaeb754efbec318f179c695202b6471934d7589 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:12:00 +0200 Subject: [PATCH 38/83] Exportable temporary assistant chats --- public/script.js | 1 + public/scripts/chats.js | 17 +++++++++++++++++ public/scripts/templates/assistantNote.html | 10 ++++++++-- public/style.css | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/public/script.js b/public/script.js index 3ba3aacbc..dc3d299da 100644 --- a/public/script.js +++ b/public/script.js @@ -724,6 +724,7 @@ async function getSystemMessages() { is_user: false, is_system: true, mes: await renderTemplateAsync('assistantNote'), + uses_system_ui: true, extra: { isSmallSys: true, }, diff --git a/public/scripts/chats.js b/public/scripts/chats.js index fbf7af3fc..94ad882bb 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -11,6 +11,7 @@ import { getCurrentChatId, getRequestHeaders, hideSwipeButtons, + name1, name2, reloadCurrentChat, saveChatDebounced, @@ -21,6 +22,7 @@ import { chat_metadata, neutralCharacterName, updateChatMetadata, + system_message_types, } from '../script.js'; import { selected_group } from './group-chats.js'; import { power_user } from './power-user.js'; @@ -34,6 +36,7 @@ import { humanFileSize, saveBase64AsFile, extractTextFromOffice, + download, } from './utils.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; @@ -41,6 +44,7 @@ import { ScraperManager } from './scrapers.js'; import { DragAndDropHandler } from './dragdrop.js'; import { renderTemplateAsync } from './templates.js'; import { t } from './i18n.js'; +import { humanizedDateTime } from './RossAscends-mods.js'; /** * @typedef {Object} FileAttachment @@ -1437,6 +1441,19 @@ jQuery(function () { await viewMessageFile(messageId); }); + $(document).on('click', '.assistant_note_export', async function () { + const chatToSave = [ + { + user_name: name1, + character_name: name2, + chat_metadata: chat_metadata, + }, + ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE), + ]; + + download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json'); + }); + // Do not change. #attachFile is added by extension. $(document).on('click', '#attachFile', function () { $('#file_form_input').trigger('click'); diff --git a/public/scripts/templates/assistantNote.html b/public/scripts/templates/assistantNote.html index 5984f8f26..f17211245 100644 --- a/public/scripts/templates/assistantNote.html +++ b/public/scripts/templates/assistantNote.html @@ -1,3 +1,9 @@ -
- Note: this chat is temporary and will be deleted as soon as you leave it. +
+
+ Note: this chat is temporary and will be deleted as soon as you leave it. + Click the button to save it as a file. +
+
diff --git a/public/style.css b/public/style.css index cbfc96185..7f4603490 100644 --- a/public/style.css +++ b/public/style.css @@ -5764,3 +5764,17 @@ body:not(.movingUI) .drawer-content.maximized { .alternate_greetings_list { overflow-y: scroll; } + +.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + gap: 10px; + padding: 0 2px; +} + +.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { + flex: 1; +} From 03c98fb55ac95fb051ef98b48e6e787d54ff9eb0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:56:44 +0200 Subject: [PATCH 39/83] OpenRouter: Support reasoning blocks --- public/index.html | 2 +- public/script.js | 23 ++++++++++------------ public/scripts/constants.js | 5 ----- public/scripts/openai.js | 5 +++++ public/scripts/sse-stream.js | 15 ++++++++++++++ src/constants.js | 5 ----- src/endpoints/backends/chat-completions.js | 13 ++++++------ 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/public/index.html b/public/index.html index a5981a186..e1b055692 100644 --- a/public/index.html +++ b/public/index.html @@ -1977,7 +1977,7 @@
-
+
+
+

+ Reasoning +

+
+ +
+
+ Prefix + +
+
+ Suffix + +
+
+
+
+ Separator + +
+
+ Max Additions + +
+
+
+

Miscellaneous

@@ -6221,11 +6254,10 @@
- + + - +
diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index 96124e043..342624c8a 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -1385,7 +1385,7 @@ "enable_functions_desc_1": "Autorise l'utilisation", "enable_functions_desc_2": "outils de fonction", "enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.", - "Show model thoughts": "Afficher les pensées du modèle", + "Show model reasoning": "Afficher les pensées du modèle", "Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.", "Confirm token parsing with": "Confirmer l'analyse des tokens avec", "openai_logit_bias_no_items": "Aucun élément", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 04129749c..4df2fd60a 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -266,7 +266,7 @@ "Use system prompt": "使用系统提示词", "Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过", "Merges_all_system_messages_desc_2": "字段发送。", - "Show model thoughts": "展示思维链", + "Show model reasoning": "展示思维链", "Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。", "Assistant Prefill": "AI预填", "Expand the editor": "展开编辑器", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 5d8313c1d..f1656afe7 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -2357,7 +2357,7 @@ "Forbid": "禁止", "Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。", "Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。", - "Show model thoughts": "顯示模型思維鏈", + "Show model reasoning": "顯示模型思維鏈", "Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。", "Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]", "Model ID (optional)": "模型 ID(可選)", diff --git a/public/script.js b/public/script.js index c18d047b1..f91070709 100644 --- a/public/script.js +++ b/public/script.js @@ -238,7 +238,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js'; -import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js'; +import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, PromptReasoning } from './scripts/chats.js'; import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; @@ -3844,6 +3844,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro coreChat.pop(); } + const reasoning = new PromptReasoning(); + for (let i = coreChat.length - 1; i >= 0; i--) { + coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) }; + } + coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { let message = chatItem.mes; let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; @@ -8034,7 +8039,7 @@ function updateEditArrowClasses() { } } -function closeMessageEditor() { +export function closeMessageEditor() { if (this_edit_mes_id) { $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 43dbb9b0c..f18f5c883 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -24,6 +24,8 @@ import { updateChatMetadata, system_message_types, updateMessageBlock, + closeMessageEditor, + substituteParams, } from '../script.js'; import { selected_group } from './group-chats.js'; import { power_user } from './power-user.js'; @@ -1418,6 +1420,47 @@ export function registerFileConverter(mimeType, converter) { converters[mimeType] = converter; } +/** + * Helper class for adding reasoning to messages. + * Keeps track of the number of reasoning additions. + */ +export class PromptReasoning { + static REASONING_PLACEHOLDER = '\u200B'; + + constructor() { + this.counter = 0; + } + + /** + * Add reasoning to a message according to the power user settings. + * @param {string} content Message content + * @param {string} reasoning Message reasoning + * @returns {string} Message content with reasoning + */ + addToMessage(content, reasoning) { + // Disabled or reached limit of additions + if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) { + return content; + } + + // No reasoning provided or a placeholder + if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { + return content; + } + + // Increment the counter + this.counter++; + + // Substitute macros in variable parts + const prefix = substituteParams(power_user.reasoning.prefix || ''); + const separator = substituteParams(power_user.reasoning.separator || ''); + const suffix = substituteParams(power_user.reasoning.suffix || ''); + + // Combine parts with reasoning and content + return `${prefix}${reasoning}${suffix}${separator}${content}`; + } +} + jQuery(function () { $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); @@ -1574,6 +1617,25 @@ jQuery(function () { e.preventDefault(); }); + $(document).on('click', '.mes_edit_add_reasoning', async function () { + const mesBlock = $(this).closest('.mes'); + const mesId = Number(mesBlock.attr('mesid')); + const message = chat[mesId]; + if (!message?.extra){ + return; + } + + if (message.extra.reasoning) { + toastr.info(t`Reasoning already exists.`, t`Edit Message`); + return; + } + + message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; + await saveChatConditional(); + closeMessageEditor(); + updateMessageBlock(mesId, message); + }); + $(document).on('click', '.mes_reasoning_delete', async function (e) { e.stopPropagation(); e.preventDefault(); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index bfe8eca2e..4859c0c71 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -253,6 +253,14 @@ let power_user = { content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.', }, + reasoning: { + add_to_prompts: false, + prefix: '\n', + suffix: '\n', + separator: '\n', + max_additions: 1, + }, + personas: {}, default_persona: null, persona_descriptions: {}, @@ -1613,6 +1621,7 @@ async function loadPowerUserSettings(settings, data) { loadMovingUIState(); loadCharListState(); toggleMDHotkeyIconDisplay(); + loadReasoningSettings(); } function toggleMDHotkeyIconDisplay() { @@ -1629,6 +1638,38 @@ function loadCharListState() { document.body.classList.toggle('charListGrid', power_user.charListGrid); } +function loadReasoningSettings() { + $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); + $('#reasoning_add_to_prompts').on('change', function () { + power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#reasoning_prefix').val(power_user.reasoning.prefix); + $('#reasoning_prefix').on('input', function () { + power_user.reasoning.prefix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_suffix').val(power_user.reasoning.suffix); + $('#reasoning_suffix').on('input', function () { + power_user.reasoning.suffix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_separator').val(power_user.reasoning.separator); + $('#reasoning_separator').on('input', function () { + power_user.reasoning.separator = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_max_additions').val(power_user.reasoning.max_additions); + $('#reasoning_max_additions').on('input', function () { + power_user.reasoning.max_additions = Number($(this).val()); + saveSettingsDebounced(); + }); +} + function loadMovingUIState() { if (!isMobile() && power_user.movingUIState From f78bf5e46f379d3c0cdc369d0ee8c273cb9cfed6 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 25 Jan 2025 21:45:55 +0100 Subject: [PATCH 61/83] Add "StartDev.bat" placeholder to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7d48fd879..4bc8ee400 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ public/scripts/extensions/third-party /certs .aider* .env +/StartDev.bat + From 44ade6ad640c3bfb4f4216e6f2341ef362ff23ae Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 25 Jan 2025 23:20:26 +0200 Subject: [PATCH 62/83] Customize CSRF token error message --- server.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server.js b/server.js index 26dcb4e0d..f9ef70798 100644 --- a/server.js +++ b/server.js @@ -402,6 +402,10 @@ if (!disableCsrf) { }); }); + // Customize the error message + csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.'); + csrfSyncProtection.invalidCsrfTokenError.stack = undefined; + app.use(csrfSyncProtection.csrfSynchronisedProtection); } else { console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); From 96143177ced2bb8c283af0510268f6582599950d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 01:42:08 +0200 Subject: [PATCH 63/83] Fix logit bias for DeepSeek on OpenRouter --- src/endpoints/backends/chat-completions.js | 10 ++++++ src/endpoints/tokenizers.js | 42 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 9dcaf4fcf..884a95c65 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -37,6 +37,8 @@ import { getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS, + webTokenizers, + getWebTokenizer, } from '../tokenizers.js'; const API_OPENAI = 'https://api.openai.com/v1'; @@ -863,6 +865,14 @@ router.post('/bias', jsonParser, async function (request, response) { return response.send({}); } encodeFunction = (text) => new Uint32Array(instance.encodeIds(text)); + } else if (webTokenizers.includes(model)) { + const tokenizer = getWebTokenizer(model); + const instance = await tokenizer?.get(); + if (!instance) { + console.warn('Tokenizer not initialized:', model); + return response.send({}); + } + encodeFunction = (text) => new Uint32Array(instance.encode(text)); } else { const tokenizer = getTiktokenTokenizer(model); encodeFunction = (tokenizer.encode.bind(tokenizer)); diff --git a/src/endpoints/tokenizers.js b/src/endpoints/tokenizers.js index bd6fdeec0..5f693b751 100644 --- a/src/endpoints/tokenizers.js +++ b/src/endpoints/tokenizers.js @@ -238,6 +238,15 @@ export const sentencepieceTokenizers = [ 'jamba', ]; +export const webTokenizers = [ + 'claude', + 'llama3', + 'command-r', + 'qwen2', + 'nemo', + 'deepseek', +]; + /** * Gets the Sentencepiece tokenizer by the model name. * @param {string} model Sentencepiece model name @@ -275,6 +284,39 @@ export function getSentencepiceTokenizer(model) { return null; } +/** + * Gets the Web tokenizer by the model name. + * @param {string} model Web tokenizer model name + * @returns {WebTokenizer|null} Web tokenizer + */ +export function getWebTokenizer(model) { + if (model.includes('llama3')) { + return llama3_tokenizer; + } + + if (model.includes('claude')) { + return claude_tokenizer; + } + + if (model.includes('command-r')) { + return commandTokenizer; + } + + if (model.includes('qwen2')) { + return qwen2Tokenizer; + } + + if (model.includes('nemo')) { + return nemoTokenizer; + } + + if (model.includes('deepseek')) { + return deepseekTokenizer; + } + + return null; +} + /** * Counts the token ids for the given text using the Sentencepiece tokenizer. * @param {SentencePieceTokenizer} tokenizer Sentencepiece tokenizer From 9aac5a22f174644504883e84abb6ba356c26032b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 01:46:30 +0200 Subject: [PATCH 64/83] Defer middleware HTML file reads --- src/middleware/basicAuth.js | 12 ++++++------ src/middleware/whitelist.js | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/middleware/basicAuth.js b/src/middleware/basicAuth.js index 4548a3f20..b75856289 100644 --- a/src/middleware/basicAuth.js +++ b/src/middleware/basicAuth.js @@ -10,13 +10,13 @@ import { getConfig, getConfigValue, safeReadFileSync } from '../util.js'; const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false); const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); -const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? ''; -const unauthorizedResponse = (res) => { - res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); - return res.status(401).send(unauthorizedWebpage); -}; - const basicAuthMiddleware = async function (request, response, callback) { + const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? ''; + const unauthorizedResponse = (res) => { + res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"'); + return res.status(401).send(unauthorizedWebpage); + }; + const config = getConfig(); const authHeader = request.headers.authorization; diff --git a/src/middleware/whitelist.js b/src/middleware/whitelist.js index 2922c42b1..4df5a6798 100644 --- a/src/middleware/whitelist.js +++ b/src/middleware/whitelist.js @@ -11,9 +11,6 @@ const whitelistPath = path.join(process.cwd(), './whitelist.txt'); const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); let whitelist = getConfigValue('whitelist', []); let knownIPs = new Set(); -const forbiddenWebpage = Handlebars.compile( - safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', -); if (fs.existsSync(whitelistPath)) { try { @@ -56,6 +53,10 @@ function getForwardedIp(req) { * @returns {import('express').RequestHandler} The middleware function */ export default function whitelistMiddleware(whitelistMode, listen) { + const forbiddenWebpage = Handlebars.compile( + safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', + ); + return function (req, res, next) { const clientIp = getIpFromRequest(req); const forwardedIp = getForwardedIp(req); From 8fc880b69b88667f8120266669369dad2921e73e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 02:07:50 +0200 Subject: [PATCH 65/83] Early stopping if prompt reasoning limit reached --- public/script.js | 3 +++ public/scripts/chats.js | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/public/script.js b/public/script.js index f91070709..b0ed60704 100644 --- a/public/script.js +++ b/public/script.js @@ -3846,6 +3846,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro const reasoning = new PromptReasoning(); for (let i = coreChat.length - 1; i >= 0; i--) { + if (reasoning.isLimitReached()) { + break; + } coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) }; } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index f18f5c883..ef1db0732 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1431,6 +1431,18 @@ export class PromptReasoning { this.counter = 0; } + /** + * Checks if the limit of reasoning additions has been reached. + * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. + */ + isLimitReached() { + if (!power_user.reasoning.add_to_prompts) { + return true; + } + + return this.counter >= power_user.reasoning.max_additions; + } + /** * Add reasoning to a message according to the power user settings. * @param {string} content Message content From 45d4d1bb3ef2f544f0d652f598d590067dd32348 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 02:49:10 +0200 Subject: [PATCH 66/83] [wip] Open reasoning editor --- public/index.html | 3 +++ public/script.js | 2 +- public/scripts/chats.js | 48 ++++++++++++++++++++++++++++++----------- public/style.css | 27 +++++++++++++---------- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/public/index.html b/public/index.html index 7bb533dc7..c7cc4c617 100644 --- a/public/index.html +++ b/public/index.html @@ -6265,6 +6265,9 @@ Reasoning
+
+
+
diff --git a/public/script.js b/public/script.js index b0ed60704..b4fd28f2f 100644 --- a/public/script.js +++ b/public/script.js @@ -11274,7 +11274,7 @@ jQuery(async function () { $(document).keyup(function (e) { if (e.key === 'Escape') { - const isEditVisible = $('#curEditTextarea').is(':visible'); + const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length; if (isEditVisible && power_user.auto_save_msg_edits === false) { closeMessageEditor(); $('#send_textarea').focus(); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index ef1db0732..5b2db51c5 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1474,6 +1474,18 @@ export class PromptReasoning { } jQuery(function () { + /** + * Gets a message from a jQuery element. + * @param {Element} element + * @returns {{messageId: number, message: object, messageBlock: JQuery}} + */ + const getMessageFromJquery = function (element) { + const messageBlock = $(element).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + const message = chat[messageId]; + return { messageId: messageId, message, messageBlock }; + }; + $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); @@ -1629,11 +1641,25 @@ jQuery(function () { e.preventDefault(); }); + $(document).on('click', '.mes_reasoning_edit', function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const reasoning = message?.extra?.reasoning; + const textarea = document.createElement('textarea'); + const reasoningBlock = messageBlock.find('.mes_reasoning'); + textarea.classList.add('reasoning_edit_textarea'); + textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; + $(textarea).insertBefore(reasoningBlock); + }); + $(document).on('click', '.mes_edit_add_reasoning', async function () { - const mesBlock = $(this).closest('.mes'); - const mesId = Number(mesBlock.attr('mesid')); - const message = chat[mesId]; - if (!message?.extra){ + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { return; } @@ -1645,7 +1671,7 @@ jQuery(function () { message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; await saveChatConditional(); closeMessageEditor(); - updateMessageBlock(mesId, message); + updateMessageBlock(messageId, message); }); $(document).on('click', '.mes_reasoning_delete', async function (e) { @@ -1658,21 +1684,17 @@ jQuery(function () { return; } - const mesBlock = $(this).closest('.mes'); - const mesId = Number(mesBlock.attr('mesid')); - const message = chat[mesId]; - if (!message?.extra){ + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { return; } message.extra.reasoning = ''; await saveChatConditional(); - updateMessageBlock(mesId, message); + updateMessageBlock(messageId, message); }); $(document).on('pointerup', '.mes_reasoning_copy', async function () { - const mesBlock = $(this).closest('.mes'); - const mesId = Number(mesBlock.attr('mesid')); - const message = chat[mesId]; + const { message } = getMessageFromJquery(this); const reasoning = message?.extra?.reasoning; if (!reasoning) { diff --git a/public/style.css b/public/style.css index 2b7ce0a01..bd31aba7f 100644 --- a/public/style.css +++ b/public/style.css @@ -353,9 +353,18 @@ input[type='checkbox']:focus-visible { .mes_reasoning_summary { cursor: pointer; position: relative; + margin: 2px; } -.mes_reasoning_details:not([open]) .mes_reasoning_actions { +.mes_bias:empty, +.mes_reasoning:empty, +.mes_reasoning_details:has(.mes_reasoning:empty), +.mes_block:has(.edit_textarea) .mes_reasoning_details, +.mes_reasoning_details:not([open]) .mes_reasoning_actions, +.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning, +.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_done, +.mes_reasoning_details:not(:has(.reasoning_edit_textarea)) .mes_reasoning_actions .mes_button.mes_reasoning_edit_cancel, +.mes_reasoning_details:has(.reasoning_edit_textarea) .mes_reasoning_actions .mes_button:not(.mes_reasoning_edit_done, .mes_reasoning_edit_cancel) { display: none; } @@ -373,21 +382,14 @@ input[type='checkbox']:focus-visible { padding: 1px; } -.mes_reasoning_summary > span { +.mes_reasoning_summary>span { margin-left: 0.5em; } -.mes_reasoning_details:has(.mes_reasoning:empty), -.mes_block:has(.edit_textarea) .mes_reasoning_details, -.mes_bias:empty, -.mes_reasoning:empty { - display: none; -} - .mes_text i, .mes_text em, .mes_reasoning i, -.mes_reasoning em { +.mes_reasoning em { color: var(--SmartThemeEmColor); } @@ -408,7 +410,7 @@ input[type='checkbox']:focus-visible { .mes_reasoning font[color] em, .mes_reasoning font[color] i, .mes_reasoning font[color] u, -.mes_reasoning font[color] q { +.mes_reasoning font[color] q { color: inherit; } @@ -4213,10 +4215,12 @@ input[type="range"]::-webkit-slider-thumb { align-items: center; } +.mes_reasoning_edit_cancel, .mes_edit_cancel.menu_button { background-color: var(--crimson70a); } +.mes_reasoning_edit_done, .mes_edit_done.menu_button { background-color: var(--okGreen70a); } @@ -4225,6 +4229,7 @@ input[type="range"]::-webkit-slider-thumb { opacity: 1; } +.reasoning_edit_textarea, .edit_textarea { padding: 5px; margin: 0; From 17d4175b47bb274048f2503ecb8e423836265308 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 05:14:17 +0200 Subject: [PATCH 67/83] Functional reasoning edit --- public/script.js | 27 +++++++++++++++++----- public/scripts/chats.js | 50 +++++++++++++++++++++++++++++++++++++++++ public/style.css | 10 +++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/public/script.js b/public/script.js index b4fd28f2f..037ff7bc3 100644 --- a/public/script.js +++ b/public/script.js @@ -5686,7 +5686,7 @@ function parseAndSaveLogprobs(data, continueFrom) { * @param {object} data Response data * @returns {string} Extracted message */ -function extractMessageFromData(data){ +function extractMessageFromData(data) { if (typeof data === 'string') { return data; } @@ -8042,9 +8042,23 @@ function updateEditArrowClasses() { } } -export function closeMessageEditor() { - if (this_edit_mes_id) { - $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); +/** + * Closes the message editor. + * @param {'message'|'reasoning'|'all'} what What to close. Default is 'all'. + */ +export function closeMessageEditor(what = 'all') { + if (what === 'message' || what === 'all') { + if (this_edit_mes_id) { + $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); + } + } + if (what === 'reasoning' || what === 'all') { + document.querySelectorAll('.reasoning_edit_textarea').forEach((el) => { + const cancelButton = el.closest('.mes')?.querySelector('.mes_reasoning_edit_cancel'); + if (cancelButton instanceof HTMLElement) { + cancelButton.click(); + } + }); } } @@ -11274,14 +11288,15 @@ jQuery(async function () { $(document).keyup(function (e) { if (e.key === 'Escape') { - const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length; + const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0; if (isEditVisible && power_user.auto_save_msg_edits === false) { - closeMessageEditor(); + closeMessageEditor('all'); $('#send_textarea').focus(); return; } if (isEditVisible && power_user.auto_save_msg_edits === true) { $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click(); + closeMessageEditor('reasoning'); $('#send_textarea').focus(); return; } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 5b2db51c5..8d6ec6040 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1650,11 +1650,61 @@ jQuery(function () { } const reasoning = message?.extra?.reasoning; + const chatElement = document.getElementById('chat'); const textarea = document.createElement('textarea'); const reasoningBlock = messageBlock.find('.mes_reasoning'); textarea.classList.add('reasoning_edit_textarea'); textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; $(textarea).insertBefore(reasoningBlock); + + if (!CSS.supports('field-sizing', 'content')) { + const resetHeight = function () { + const scrollTop = chatElement.scrollTop; + textarea.style.height = '0px'; + textarea.style.height = `${textarea.scrollHeight}px`; + chatElement.scrollTop = scrollTop; + }; + + textarea.addEventListener('input', resetHeight); + resetHeight(); + } + + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + + const textareaRect = textarea.getBoundingClientRect(); + const chatRect = chatElement.getBoundingClientRect(); + + // Scroll if textarea bottom is below visible area + if (textareaRect.bottom > chatRect.bottom) { + const scrollOffset = textareaRect.bottom - chatRect.bottom; + chatElement.scrollTop += scrollOffset; + } + }); + + $(document).on('click', '.mes_reasoning_edit_done', async function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageId, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const textarea = messageBlock.find('.reasoning_edit_textarea'); + const reasoning = String(textarea.val()); + message.extra.reasoning = reasoning; + await saveChatConditional(); + updateMessageBlock(messageId, message); + textarea.remove(); + }); + + $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { + e.stopPropagation(); + e.preventDefault(); + + const { messageBlock } = getMessageFromJquery(this); + const textarea = messageBlock.find('.reasoning_edit_textarea'); + textarea.remove(); }); $(document).on('click', '.mes_edit_add_reasoning', async function () { diff --git a/public/style.css b/public/style.css index bd31aba7f..0318e6d7d 100644 --- a/public/style.css +++ b/public/style.css @@ -356,6 +356,12 @@ input[type='checkbox']:focus-visible { margin: 2px; } +@supports not selector(:has(*)) { + .mes_reasoning_details { + display: none !important; + } +} + .mes_bias:empty, .mes_reasoning:empty, .mes_reasoning_details:has(.mes_reasoning:empty), @@ -1089,6 +1095,10 @@ body .panelControlBar { /*only affects bubblechat to make it sit nicely at the bottom*/ } +.last_mes:has(.mes_text:empty):has(.mes_reasoning_details[open]) .mes_reasoning:not(:empty) { + margin-bottom: 30px; +} + .last_mes .mes_reasoning, .last_mes .mes_text { padding-right: 30px; From eb798fa4f191892bffe07d40bd255b69dddf2d4f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:47:13 +0200 Subject: [PATCH 68/83] Move reasoning-specific code into its own module --- public/script.js | 4 +- public/scripts/chats.js | 188 ----------------------------- public/scripts/power-user.js | 33 ----- public/scripts/reasoning.js | 228 +++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 222 deletions(-) create mode 100644 public/scripts/reasoning.js diff --git a/public/script.js b/public/script.js index 037ff7bc3..43245e7a3 100644 --- a/public/script.js +++ b/public/script.js @@ -238,7 +238,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js'; -import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, PromptReasoning } from './scripts/chats.js'; +import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js'; import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; @@ -267,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js'; import { initBulkEdit } from './scripts/bulk-edit.js'; import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; import { getContext } from './scripts/st-context.js'; +import { initReasoning, PromptReasoning } from './scripts/reasoning.js'; // API OBJECT FOR EXTERNAL WIRING globalThis.SillyTavern = { @@ -982,6 +983,7 @@ async function firstLoadInit() { initServerHistory(); initSettingsSearch(); initBulkEdit(); + initReasoning(); await initScrapers(); doDailyExtensionUpdatesCheck(); await hideLoader(); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 8d6ec6040..94ad882bb 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -23,9 +23,6 @@ import { neutralCharacterName, updateChatMetadata, system_message_types, - updateMessageBlock, - closeMessageEditor, - substituteParams, } from '../script.js'; import { selected_group } from './group-chats.js'; import { power_user } from './power-user.js'; @@ -40,7 +37,6 @@ import { saveBase64AsFile, extractTextFromOffice, download, - copyText, } from './utils.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; @@ -1420,72 +1416,7 @@ export function registerFileConverter(mimeType, converter) { converters[mimeType] = converter; } -/** - * Helper class for adding reasoning to messages. - * Keeps track of the number of reasoning additions. - */ -export class PromptReasoning { - static REASONING_PLACEHOLDER = '\u200B'; - - constructor() { - this.counter = 0; - } - - /** - * Checks if the limit of reasoning additions has been reached. - * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. - */ - isLimitReached() { - if (!power_user.reasoning.add_to_prompts) { - return true; - } - - return this.counter >= power_user.reasoning.max_additions; - } - - /** - * Add reasoning to a message according to the power user settings. - * @param {string} content Message content - * @param {string} reasoning Message reasoning - * @returns {string} Message content with reasoning - */ - addToMessage(content, reasoning) { - // Disabled or reached limit of additions - if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) { - return content; - } - - // No reasoning provided or a placeholder - if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { - return content; - } - - // Increment the counter - this.counter++; - - // Substitute macros in variable parts - const prefix = substituteParams(power_user.reasoning.prefix || ''); - const separator = substituteParams(power_user.reasoning.separator || ''); - const suffix = substituteParams(power_user.reasoning.suffix || ''); - - // Combine parts with reasoning and content - return `${prefix}${reasoning}${suffix}${separator}${content}`; - } -} - jQuery(function () { - /** - * Gets a message from a jQuery element. - * @param {Element} element - * @returns {{messageId: number, message: object, messageBlock: JQuery}} - */ - const getMessageFromJquery = function (element) { - const messageBlock = $(element).closest('.mes'); - const messageId = Number(messageBlock.attr('mesid')); - const message = chat[messageId]; - return { messageId: messageId, message, messageBlock }; - }; - $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); const messageId = Number(messageBlock.attr('mesid')); @@ -1636,125 +1567,6 @@ jQuery(function () { $(document).on('click', '.mes_img_enlarge', enlargeMessageImage); $(document).on('click', '.mes_img_delete', deleteMessageImage); - $(document).on('click', '.mes_reasoning_copy', (e) => { - e.stopPropagation(); - e.preventDefault(); - }); - - $(document).on('click', '.mes_reasoning_edit', function (e) { - e.stopPropagation(); - e.preventDefault(); - const { message, messageBlock } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - - const reasoning = message?.extra?.reasoning; - const chatElement = document.getElementById('chat'); - const textarea = document.createElement('textarea'); - const reasoningBlock = messageBlock.find('.mes_reasoning'); - textarea.classList.add('reasoning_edit_textarea'); - textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; - $(textarea).insertBefore(reasoningBlock); - - if (!CSS.supports('field-sizing', 'content')) { - const resetHeight = function () { - const scrollTop = chatElement.scrollTop; - textarea.style.height = '0px'; - textarea.style.height = `${textarea.scrollHeight}px`; - chatElement.scrollTop = scrollTop; - }; - - textarea.addEventListener('input', resetHeight); - resetHeight(); - } - - textarea.focus(); - textarea.setSelectionRange(textarea.value.length, textarea.value.length); - - const textareaRect = textarea.getBoundingClientRect(); - const chatRect = chatElement.getBoundingClientRect(); - - // Scroll if textarea bottom is below visible area - if (textareaRect.bottom > chatRect.bottom) { - const scrollOffset = textareaRect.bottom - chatRect.bottom; - chatElement.scrollTop += scrollOffset; - } - }); - - $(document).on('click', '.mes_reasoning_edit_done', async function (e) { - e.stopPropagation(); - e.preventDefault(); - const { message, messageId, messageBlock } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - - const textarea = messageBlock.find('.reasoning_edit_textarea'); - const reasoning = String(textarea.val()); - message.extra.reasoning = reasoning; - await saveChatConditional(); - updateMessageBlock(messageId, message); - textarea.remove(); - }); - - $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { - e.stopPropagation(); - e.preventDefault(); - - const { messageBlock } = getMessageFromJquery(this); - const textarea = messageBlock.find('.reasoning_edit_textarea'); - textarea.remove(); - }); - - $(document).on('click', '.mes_edit_add_reasoning', async function () { - const { message, messageId } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - - if (message.extra.reasoning) { - toastr.info(t`Reasoning already exists.`, t`Edit Message`); - return; - } - - message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; - await saveChatConditional(); - closeMessageEditor(); - updateMessageBlock(messageId, message); - }); - - $(document).on('click', '.mes_reasoning_delete', async function (e) { - e.stopPropagation(); - e.preventDefault(); - - const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`); - - if (!confirm) { - return; - } - - const { message, messageId } = getMessageFromJquery(this); - if (!message?.extra) { - return; - } - message.extra.reasoning = ''; - await saveChatConditional(); - updateMessageBlock(messageId, message); - }); - - $(document).on('pointerup', '.mes_reasoning_copy', async function () { - const { message } = getMessageFromJquery(this); - const reasoning = message?.extra?.reasoning; - - if (!reasoning) { - return; - } - - await copyText(reasoning); - toastr.info(t`Copied!`, '', { timeOut: 2000 }); - }); - $('#file_form_input').on('change', async () => { const fileInput = document.getElementById('file_form_input'); if (!(fileInput instanceof HTMLInputElement)) return; diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 4859c0c71..08840c7bc 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1621,7 +1621,6 @@ async function loadPowerUserSettings(settings, data) { loadMovingUIState(); loadCharListState(); toggleMDHotkeyIconDisplay(); - loadReasoningSettings(); } function toggleMDHotkeyIconDisplay() { @@ -1638,38 +1637,6 @@ function loadCharListState() { document.body.classList.toggle('charListGrid', power_user.charListGrid); } -function loadReasoningSettings() { - $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); - $('#reasoning_add_to_prompts').on('change', function () { - power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); - saveSettingsDebounced(); - }); - - $('#reasoning_prefix').val(power_user.reasoning.prefix); - $('#reasoning_prefix').on('input', function () { - power_user.reasoning.prefix = String($(this).val()); - saveSettingsDebounced(); - }); - - $('#reasoning_suffix').val(power_user.reasoning.suffix); - $('#reasoning_suffix').on('input', function () { - power_user.reasoning.suffix = String($(this).val()); - saveSettingsDebounced(); - }); - - $('#reasoning_separator').val(power_user.reasoning.separator); - $('#reasoning_separator').on('input', function () { - power_user.reasoning.separator = String($(this).val()); - saveSettingsDebounced(); - }); - - $('#reasoning_max_additions').val(power_user.reasoning.max_additions); - $('#reasoning_max_additions').on('input', function () { - power_user.reasoning.max_additions = Number($(this).val()); - saveSettingsDebounced(); - }); -} - function loadMovingUIState() { if (!isMobile() && power_user.movingUIState diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js new file mode 100644 index 000000000..0d2011b12 --- /dev/null +++ b/public/scripts/reasoning.js @@ -0,0 +1,228 @@ +import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; +import { t } from './i18n.js'; +import { Popup } from './popup.js'; +import { power_user } from './power-user.js'; +import { copyText } from './utils.js'; + +/** + * Gets a message from a jQuery element. + * @param {Element} element + * @returns {{messageId: number, message: object, messageBlock: JQuery}} + */ +function getMessageFromJquery(element) { + const messageBlock = $(element).closest('.mes'); + const messageId = Number(messageBlock.attr('mesid')); + const message = chat[messageId]; + return { messageId: messageId, message, messageBlock }; +} + +/** + * Helper class for adding reasoning to messages. + * Keeps track of the number of reasoning additions. + */ +export class PromptReasoning { + static REASONING_PLACEHOLDER = '\u200B'; + + constructor() { + this.counter = 0; + } + + /** + * Checks if the limit of reasoning additions has been reached. + * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise. + */ + isLimitReached() { + if (!power_user.reasoning.add_to_prompts) { + return true; + } + + return this.counter >= power_user.reasoning.max_additions; + } + + /** + * Add reasoning to a message according to the power user settings. + * @param {string} content Message content + * @param {string} reasoning Message reasoning + * @returns {string} Message content with reasoning + */ + addToMessage(content, reasoning) { + // Disabled or reached limit of additions + if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) { + return content; + } + + // No reasoning provided or a placeholder + if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { + return content; + } + + // Increment the counter + this.counter++; + + // Substitute macros in variable parts + const prefix = substituteParams(power_user.reasoning.prefix || ''); + const separator = substituteParams(power_user.reasoning.separator || ''); + const suffix = substituteParams(power_user.reasoning.suffix || ''); + + // Combine parts with reasoning and content + return `${prefix}${reasoning}${suffix}${separator}${content}`; + } +} + +function loadReasoningSettings() { + $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); + $('#reasoning_add_to_prompts').on('change', function () { + power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#reasoning_prefix').val(power_user.reasoning.prefix); + $('#reasoning_prefix').on('input', function () { + power_user.reasoning.prefix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_suffix').val(power_user.reasoning.suffix); + $('#reasoning_suffix').on('input', function () { + power_user.reasoning.suffix = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_separator').val(power_user.reasoning.separator); + $('#reasoning_separator').on('input', function () { + power_user.reasoning.separator = String($(this).val()); + saveSettingsDebounced(); + }); + + $('#reasoning_max_additions').val(power_user.reasoning.max_additions); + $('#reasoning_max_additions').on('input', function () { + power_user.reasoning.max_additions = Number($(this).val()); + saveSettingsDebounced(); + }); +} + +function setReasoningEventHandlers(){ + $(document).on('click', '.mes_reasoning_copy', (e) => { + e.stopPropagation(); + e.preventDefault(); + }); + + $(document).on('click', '.mes_reasoning_edit', function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const reasoning = message?.extra?.reasoning; + const chatElement = document.getElementById('chat'); + const textarea = document.createElement('textarea'); + const reasoningBlock = messageBlock.find('.mes_reasoning'); + textarea.classList.add('reasoning_edit_textarea'); + textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; + $(textarea).insertBefore(reasoningBlock); + + if (!CSS.supports('field-sizing', 'content')) { + const resetHeight = function () { + const scrollTop = chatElement.scrollTop; + textarea.style.height = '0px'; + textarea.style.height = `${textarea.scrollHeight}px`; + chatElement.scrollTop = scrollTop; + }; + + textarea.addEventListener('input', resetHeight); + resetHeight(); + } + + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + + const textareaRect = textarea.getBoundingClientRect(); + const chatRect = chatElement.getBoundingClientRect(); + + // Scroll if textarea bottom is below visible area + if (textareaRect.bottom > chatRect.bottom) { + const scrollOffset = textareaRect.bottom - chatRect.bottom; + chatElement.scrollTop += scrollOffset; + } + }); + + $(document).on('click', '.mes_reasoning_edit_done', async function (e) { + e.stopPropagation(); + e.preventDefault(); + const { message, messageId, messageBlock } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + const textarea = messageBlock.find('.reasoning_edit_textarea'); + const reasoning = String(textarea.val()); + message.extra.reasoning = reasoning; + await saveChatConditional(); + updateMessageBlock(messageId, message); + textarea.remove(); + }); + + $(document).on('click', '.mes_reasoning_edit_cancel', function (e) { + e.stopPropagation(); + e.preventDefault(); + + const { messageBlock } = getMessageFromJquery(this); + const textarea = messageBlock.find('.reasoning_edit_textarea'); + textarea.remove(); + }); + + $(document).on('click', '.mes_edit_add_reasoning', async function () { + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + + if (message.extra.reasoning) { + toastr.info(t`Reasoning already exists.`, t`Edit Message`); + return; + } + + message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER; + await saveChatConditional(); + closeMessageEditor(); + updateMessageBlock(messageId, message); + }); + + $(document).on('click', '.mes_reasoning_delete', async function (e) { + e.stopPropagation(); + e.preventDefault(); + + const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`); + + if (!confirm) { + return; + } + + const { message, messageId } = getMessageFromJquery(this); + if (!message?.extra) { + return; + } + message.extra.reasoning = ''; + await saveChatConditional(); + updateMessageBlock(messageId, message); + }); + + $(document).on('pointerup', '.mes_reasoning_copy', async function () { + const { message } = getMessageFromJquery(this); + const reasoning = message?.extra?.reasoning; + + if (!reasoning) { + return; + } + + await copyText(reasoning); + toastr.info(t`Copied!`, '', { timeOut: 2000 }); + }); +} + +export function initReasoning() { + loadReasoningSettings(); + setReasoningEventHandlers(); +} From c9ab987658d4d961a157d7f235cfd317ad334996 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:48:04 +0200 Subject: [PATCH 69/83] Fix default thonk separator --- public/scripts/power-user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 08840c7bc..2a74f8722 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -257,7 +257,7 @@ let power_user = { add_to_prompts: false, prefix: '\n', suffix: '\n', - separator: '\n', + separator: '\n\n', max_additions: 1, }, From a42337ad0ad98c5cbf77f312776faaf41ccf4b0b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:50:59 +0200 Subject: [PATCH 70/83] Use 'localhost' as a fallback for hostname --- src/users.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/users.js b/src/users.js index 36a5d62b3..c8df39779 100644 --- a/src/users.js +++ b/src/users.js @@ -458,7 +458,8 @@ export function getPasswordSalt() { */ export function getCookieSessionName() { // Get server hostname and hash it to generate a session suffix - const suffix = crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 8); + const hostname = os.hostname() || 'localhost'; + const suffix = crypto.createHash('sha256').update(hostname).digest('hex').slice(0, 8); return `session-${suffix}`; } From a7516937f78108f5cfa589a4f5869e8c92ef5e0d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:00:14 +0200 Subject: [PATCH 71/83] Add reasoning slash commands --- public/scripts/reasoning.js | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 0d2011b12..90721cd5d 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -1,7 +1,12 @@ import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js'; import { t } from './i18n.js'; +import { MacrosParser } from './macros.js'; import { Popup } from './popup.js'; import { power_user } from './power-user.js'; +import { SlashCommand } from './slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; +import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { copyText } from './utils.js'; /** @@ -101,6 +106,67 @@ function loadReasoningSettings() { }); } +function registerReasoningSlashCommands() { + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'reasoning-get', + returns: ARGUMENT_TYPE.STRING, + helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Message ID. If not provided, the message ID of the last message is used.', + typeList: ARGUMENT_TYPE.NUMBER, + enumProvider: commonEnumProviders.messages(), + }), + ], + callback: (_args, value) => { + const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1; + const message = chat[messageId]; + const reasoning = message?.extra?.reasoning; + return reasoning !== PromptReasoning.REASONING_PLACEHOLDER ? reasoning : ''; + }, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'reasoning-set', + returns: ARGUMENT_TYPE.STRING, + helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'at', + description: 'Message ID. If not provided, the message ID of the last message is used.', + typeList: ARGUMENT_TYPE.NUMBER, + enumProvider: commonEnumProviders.messages(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Reasoning block content.', + typeList: ARGUMENT_TYPE.STRING, + }), + ], + callback: async (args, value) => { + const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1; + const message = chat[messageId]; + if (!message?.extra) { + return ''; + } + + message.extra.reasoning = String(value); + await saveChatConditional(); + + closeMessageEditor('reasoning'); + updateMessageBlock(messageId, message); + return message.extra.reasoning; + }, + })); +} + +function registerReasoningMacros() { + MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`); + MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`); + MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`); +} + function setReasoningEventHandlers(){ $(document).on('click', '.mes_reasoning_copy', (e) => { e.stopPropagation(); @@ -225,4 +291,6 @@ function setReasoningEventHandlers(){ export function initReasoning() { loadReasoningSettings(); setReasoningEventHandlers(); + registerReasoningSlashCommands(); + registerReasoningMacros(); } From bfedf20db52c1e91ab5e141e12a93d2c220b982d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:29:31 +0200 Subject: [PATCH 72/83] Add reasoning tokens to token count. --- public/script.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/public/script.js b/public/script.js index 43245e7a3..38a4d562f 100644 --- a/public/script.js +++ b/public/script.js @@ -3195,7 +3195,8 @@ class StreamingProcessor { this.#updateMessageBlockVisibility(); const currentTime = new Date(); // Don't waste time calculating token count for streaming - const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0; + const tokenCountText = (this.reasoning || '') + processedText; + const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0; const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); chat[messageId]['mes'] = processedText; chat[messageId]['gen_started'] = this.timeStarted; @@ -5936,7 +5937,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); chat[chat.length - 1]['extra']['reasoning'] = reasoning; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); @@ -5957,7 +5959,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); @@ -5975,7 +5978,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); chat[chat.length - 1]['extra']['reasoning'] += reasoning; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); @@ -6001,7 +6005,8 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, chat[chat.length - 1]['gen_finished'] = generationFinished; if (power_user.message_token_count_enabled) { - chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); + const tokenCountText = (reasoning || '') + chat[chat.length - 1]['mes']; + chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); } if (selected_group) { @@ -8544,7 +8549,8 @@ function swipe_left() { // when we swipe left..but no generation. } const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`); - const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0); + const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes; + const tokenCount = await getTokenCountAsync(tokenCountText, 0); chat[chat.length - 1]['extra']['token_count'] = tokenCount; swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`); } @@ -8719,7 +8725,8 @@ const swipe_right = () => { chat[chat.length - 1].extra = {}; } - const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0); + const tokenCountText = (chat[chat.length - 1]?.extra?.reasoning || '') + chat[chat.length - 1].mes; + const tokenCount = await getTokenCountAsync(tokenCountText, 0); chat[chat.length - 1]['extra']['token_count'] = tokenCount; swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`); } @@ -9521,7 +9528,8 @@ function addDebugFunctions() { message.extra = {}; } - message.extra.token_count = await getTokenCountAsync(message.mes, 0); + const tokenCountText = (message?.extra?.reasoning || '') + message.mes; + message.extra.token_count = await getTokenCountAsync(tokenCountText, 0); } await saveChatConditional(); From 35ab677ff12ae6d1439070ffa71d44ddbe4609dc Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 26 Jan 2025 18:44:12 +0100 Subject: [PATCH 73/83] Add char version to group panel member names - Follows the same style as version in char list - Respects the chosen "auxiliary field" setting Closes #3359 --- public/index.html | 5 ++++- public/scripts/group-chats.js | 9 +++++++++ public/style.css | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index f26c95aa0..a9b37e906 100644 --- a/public/index.html +++ b/public/index.html @@ -6328,7 +6328,10 @@ Avatar
-
+
+ + +
diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index bc7434993..c0d7f39e9 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -1368,6 +1368,15 @@ function getGroupCharacterBlock(character) { template.find('.ch_fav').val(isFav); template.toggleClass('is_fav', isFav); + const auxFieldName = power_user.aux_field || 'character_version'; + const auxFieldValue = (character.data && character.data[auxFieldName]) || ''; + if (auxFieldValue) { + template.find('.character_version').text(auxFieldValue); + } + else { + template.find('.character_version').hide(); + } + let queuePosition = groupChatQueueOrder.get(character.avatar); if (queuePosition) { template.find('.queue_position').text(queuePosition); diff --git a/public/style.css b/public/style.css index 7f4603490..431189b0e 100644 --- a/public/style.css +++ b/public/style.css @@ -2921,7 +2921,7 @@ input[type=search]:focus::-webkit-search-cancel-button { position: relative; } -#rm_print_characters_block .ch_name, +.character_name_block .ch_name, .avatar-container .ch_name { flex: 1 1 auto; white-space: nowrap; From 65e32f720d2c341a70c0826a57a67d57a6d9b77c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:58:37 +0200 Subject: [PATCH 74/83] Use default avatar if imported image is corrupted --- src/endpoints/characters.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index c7a2a02d4..cf084aca5 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -74,12 +74,17 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u * Read the image, resize, and save it as a PNG into the buffer. * @returns {Promise} Image buffer */ - function getInputImage() { - if (Buffer.isBuffer(inputFile)) { - return parseImageBuffer(inputFile, crop); - } + async function getInputImage() { + try { + if (Buffer.isBuffer(inputFile)) { + return await parseImageBuffer(inputFile, crop); + } - return tryReadImage(inputFile, crop); + return await tryReadImage(inputFile, crop); + } catch (error) { + console.log(`Failed to read image: ${inputFile}. Using a fallback image.`); + return await fs.promises.readFile(defaultAvatarPath); + } } const inputImage = await getInputImage(); From a58476d0794ed698275076df0de71551da0afdad Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:15:32 +0200 Subject: [PATCH 75/83] Clip long version strings --- public/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/style.css b/public/style.css index 431189b0e..95dcd314c 100644 --- a/public/style.css +++ b/public/style.css @@ -2931,6 +2931,13 @@ input[type=search]:focus::-webkit-search-cancel-button { display: block; } +.character_name_block .character_version { + text-overflow: ellipsis; + overflow: hidden; + text-wrap: nowrap; + max-width: 50%; +} + #rm_print_characters_block .character_name_block> :last-child { flex: 0 100000 auto; /* Force shrinking first */ From 999da4945aaf1da6f6d4ff1e9e314c11f0ccfeb1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:29:04 +0200 Subject: [PATCH 76/83] Fix error log --- src/endpoints/characters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index cf084aca5..1e5df80bd 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -82,7 +82,8 @@ async function writeCharacterData(inputFile, data, outputFile, request, crop = u return await tryReadImage(inputFile, crop); } catch (error) { - console.log(`Failed to read image: ${inputFile}. Using a fallback image.`); + const message = Buffer.isBuffer(inputFile) ? 'Failed to read image buffer.' : `Failed to read image: ${inputFile}.`; + console.warn(message, 'Using a fallback image.', error); return await fs.promises.readFile(defaultAvatarPath); } } From 312969462ecc4438cbb90f93173c2c72aa1c7116 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:35:24 +0200 Subject: [PATCH 77/83] Fix AICC website reference --- public/locales/ar-sa.json | 2 +- public/locales/de-de.json | 2 +- public/locales/es-es.json | 2 +- public/locales/fr-fr.json | 2 +- public/locales/is-is.json | 2 +- public/locales/it-it.json | 2 +- public/locales/ja-jp.json | 2 +- public/locales/ko-kr.json | 2 +- public/locales/nl-nl.json | 2 +- public/locales/pt-pt.json | 2 +- public/locales/ru-ru.json | 2 +- public/locales/uk-ua.json | 2 +- public/locales/vi-vn.json | 2 +- public/locales/zh-cn.json | 2 +- public/locales/zh-tw.json | 2 +- public/scripts/templates/importCharacters.html | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/public/locales/ar-sa.json b/public/locales/ar-sa.json index ba367ad46..46f8a2805 100644 --- a/public/locales/ar-sa.json +++ b/public/locales/ar-sa.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (رابط مباشر أو معرف)", "char_import_3": "حرف JanitorAI (رابط مباشر أو UUID)", "char_import_4": "حرف Pygmalion.chat (رابط مباشر أو UUID)", - "char_import_5": "حرف AICharacterCard.com (رابط مباشر أو معرف)", + "char_import_5": "حرف AICharacterCards.com (رابط مباشر أو معرف)", "char_import_6": "رابط PNG المباشر (راجع", "char_import_7": "للمضيفين المسموح بهم)", "char_import_8": "شخصية RisuRealm (رابط مباشر)", diff --git a/public/locales/de-de.json b/public/locales/de-de.json index 92c56c0b7..a10d80e0b 100644 --- a/public/locales/de-de.json +++ b/public/locales/de-de.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (Direktlink oder ID)", "char_import_3": "JanitorAI-Charakter (Direktlink oder UUID)", "char_import_4": "Pygmalion.chat-Charakter (Direktlink oder UUID)", - "char_import_5": "AICharacterCard.com-Charakter (Direktlink oder ID)", + "char_import_5": "AICharacterCards.com-Charakter (Direktlink oder ID)", "char_import_6": "Direkter PNG-Link (siehe", "char_import_7": "für erlaubte Hosts)", "char_import_8": "RisuRealm-Charakter (Direktlink)", diff --git a/public/locales/es-es.json b/public/locales/es-es.json index 23a8f97be..334a283b3 100644 --- a/public/locales/es-es.json +++ b/public/locales/es-es.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (enlace directo o ID)", "char_import_3": "Carácter de JanitorAI (enlace directo o UUID)", "char_import_4": "Carácter Pygmalion.chat (enlace directo o UUID)", - "char_import_5": "Carácter AICharacterCard.com (enlace directo o ID)", + "char_import_5": "Carácter AICharacterCards.com (enlace directo o ID)", "char_import_6": "Enlace PNG directo (consulte", "char_import_7": "para hosts permitidos)", "char_import_8": "Personaje RisuRealm (Enlace directo)", diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index 96124e043..b2fc0b361 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -1297,7 +1297,7 @@ "char_import_2": "Lorebook de Chub (lien direct ou ID)", "char_import_3": "Personnage de JanitorAI (lien direct ou UUID)", "char_import_4": "Personnage de Pygmalion.chat (lien direct ou UUID)", - "char_import_5": "Personnage de AICharacterCard.com (lien direct ou identifiant)", + "char_import_5": "Personnage de AICharacterCards.com (lien direct ou identifiant)", "char_import_6": "Lien PNG direct (voir", "char_import_7": "pour les hôtes autorisés)", "char_import_8": "Personnage de RisuRealm (lien direct)", diff --git a/public/locales/is-is.json b/public/locales/is-is.json index 576437e67..2cacc1511 100644 --- a/public/locales/is-is.json +++ b/public/locales/is-is.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (beinn hlekkur eða auðkenni)", "char_import_3": "JanitorAI karakter (beinn hlekkur eða UUID)", "char_import_4": "Pygmalion.chat karakter (beinn hlekkur eða UUID)", - "char_import_5": "AICharacterCard.com Karakter (beinn hlekkur eða auðkenni)", + "char_import_5": "AICharacterCards.com Karakter (beinn hlekkur eða auðkenni)", "char_import_6": "Beinn PNG hlekkur (sjá", "char_import_7": "fyrir leyfilega gestgjafa)", "char_import_8": "RisuRealm karakter (beinn hlekkur)", diff --git a/public/locales/it-it.json b/public/locales/it-it.json index 807a03e91..eeea5c0fb 100644 --- a/public/locales/it-it.json +++ b/public/locales/it-it.json @@ -1376,7 +1376,7 @@ "char_import_2": "Lorebook di Chub (collegamento diretto o ID)", "char_import_3": "Carattere JanitorAI (collegamento diretto o UUID)", "char_import_4": "Carattere Pygmalion.chat (collegamento diretto o UUID)", - "char_import_5": "Carattere AICharacterCard.com (Link diretto o ID)", + "char_import_5": "Carattere AICharacterCards.com (Link diretto o ID)", "char_import_6": "Collegamento PNG diretto (fare riferimento a", "char_import_7": "per gli host consentiti)", "char_import_8": "Personaggio RisuRealm (collegamento diretto)", diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index bd5fa8281..2b3119e6e 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -1378,7 +1378,7 @@ "char_import_2": "Chub ロアブック (直接リンクまたは ID)", "char_import_3": "JanitorAI キャラクター (直接リンクまたは UUID)", "char_import_4": "Pygmalion.chat キャラクター (直接リンクまたは UUID)", - "char_import_5": "AICharacterCard.com キャラクター (直接リンクまたは ID)", + "char_import_5": "AICharacterCards.com キャラクター (直接リンクまたは ID)", "char_import_6": "直接PNGリンク(参照", "char_import_7": "許可されたホストの場合)", "char_import_8": "RisuRealm キャラクター (直接リンク)", diff --git a/public/locales/ko-kr.json b/public/locales/ko-kr.json index 044e57841..2c2dfca86 100644 --- a/public/locales/ko-kr.json +++ b/public/locales/ko-kr.json @@ -1395,7 +1395,7 @@ "char_import_2": "Chub Lorebook(직접 링크 또는 ID)", "char_import_3": "JanitorAI 캐릭터(직접 링크 또는 UUID)", "char_import_4": "Pygmalion.chat 문자(직접 링크 또는 UUID)", - "char_import_5": "AICharacterCard.com 캐릭터(직접 링크 또는 ID)", + "char_import_5": "AICharacterCards.com 캐릭터(직접 링크 또는 ID)", "char_import_6": "직접 PNG 링크(참조", "char_import_7": "허용된 호스트의 경우)", "char_import_8": "RisuRealm 캐릭터 (직접링크)", diff --git a/public/locales/nl-nl.json b/public/locales/nl-nl.json index 327b332b0..c4820f5b8 100644 --- a/public/locales/nl-nl.json +++ b/public/locales/nl-nl.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (directe link of ID)", "char_import_3": "JanitorAI-personage (directe link of UUID)", "char_import_4": "Pygmalion.chat-teken (directe link of UUID)", - "char_import_5": "AICharacterCard.com-teken (directe link of ID)", + "char_import_5": "AICharacterCards.com-teken (directe link of ID)", "char_import_6": "Directe PNG-link (zie", "char_import_7": "voor toegestane hosts)", "char_import_8": "RisuRealm-personage (directe link)", diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json index d0f1d2681..5a2fba569 100644 --- a/public/locales/pt-pt.json +++ b/public/locales/pt-pt.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (link direto ou ID)", "char_import_3": "Personagem JanitorAI (Link Direto ou UUID)", "char_import_4": "Caractere Pygmalion.chat (Link Direto ou UUID)", - "char_import_5": "Personagem AICharacterCard.com (link direto ou ID)", + "char_import_5": "Personagem AICharacterCards.com (link direto ou ID)", "char_import_6": "Link PNG direto (consulte", "char_import_7": "para hosts permitidos)", "char_import_8": "Personagem RisuRealm (link direto)", diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json index 8e304da71..4c67beaa9 100644 --- a/public/locales/ru-ru.json +++ b/public/locales/ru-ru.json @@ -966,7 +966,7 @@ "char_import_2": "Лорбук с Chub (прямая ссылка или ID)", "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)", "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)", - "char_import_5": "Персонаж с AICharacterCard.com (прямая ссылка или ID)", + "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в", "char_import_7": ")", "Grammar String": "Грамматика", diff --git a/public/locales/uk-ua.json b/public/locales/uk-ua.json index b60945a5b..216ed3928 100644 --- a/public/locales/uk-ua.json +++ b/public/locales/uk-ua.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub Lorebook (пряме посилання або ID)", "char_import_3": "Символ JanitorAI (пряме посилання або UUID)", "char_import_4": "Символ Pygmalion.chat (пряме посилання або UUID)", - "char_import_5": "Символ AICharacterCard.com (пряме посилання або ідентифікатор)", + "char_import_5": "Символ AICharacterCards.com (пряме посилання або ідентифікатор)", "char_import_6": "Пряме посилання на PNG (див", "char_import_7": "для дозволених хостів)", "char_import_8": "Персонаж RisuRealm (пряме посилання)", diff --git a/public/locales/vi-vn.json b/public/locales/vi-vn.json index be9894621..36459e94f 100644 --- a/public/locales/vi-vn.json +++ b/public/locales/vi-vn.json @@ -1376,7 +1376,7 @@ "char_import_2": "Chub (Nhập URL trực tiếp hoặc ID)", "char_import_3": "JanitorAI (Nhập URL trực tiếp hoặc UUID)", "char_import_4": "Pygmalion.chat (Nhập URL trực tiếp hoặc UUID)", - "char_import_5": "AICharacterCard.com (Nhập URL trực tiếp hoặc ID)", + "char_import_5": "AICharacterCards.com (Nhập URL trực tiếp hoặc ID)", "char_import_6": "Nhập PNG trực tiếp (tham khảo", "char_import_7": "đối với các máy chủ được phép)", "char_import_8": "RisuRealm (URL trực tiếp)", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 04129749c..17a6ab6e8 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1829,7 +1829,7 @@ "char_import_2": "Chub 知识书(直链或ID)", "char_import_3": "JanitorAI 角色(直链或UUID)", "char_import_4": "Pygmalion.chat 角色(直链或UUID)", - "char_import_5": "AICharacterCard.com 角色(直链或ID)", + "char_import_5": "AICharacterCards.com 角色(直链或ID)", "char_import_6": "被允许的PNG直链(请参阅", "char_import_7": ")", "char_import_8": "RisuRealm 角色(直链)", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index 5d8313c1d..062aa7942 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -1381,7 +1381,7 @@ "char_import_2": "Chub Lorebook(直接連結或 ID)", "char_import_3": "JanitorAI 角色(直接連結或 ID)", "char_import_4": "Pygmalion.chat 角色(直接連結或 ID)", - "char_import_5": "AICharacterCard.com 角色(直接連結或 ID)", + "char_import_5": "AICharacterCards.com 角色(直接連結或 ID)", "char_import_6": "直接 PNG 連結(請參閱", "char_import_7": "對於允許的主機)", "char_import_8": "RisuRealm角色(直接連結)", diff --git a/public/scripts/templates/importCharacters.html b/public/scripts/templates/importCharacters.html index ee226f741..4bc4807bf 100644 --- a/public/scripts/templates/importCharacters.html +++ b/public/scripts/templates/importCharacters.html @@ -7,7 +7,7 @@
  • Chub Lorebook (Direct Link or ID)
    Example: lorebooks/bartleby/example-lorebook
  • JanitorAI Character (Direct Link or UUID)
    Example: ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess
  • Pygmalion.chat Character (Direct Link or UUID)
    Example: a7ca95a1-0c88-4e23-91b3-149db1e78ab9
  • -
  • AICharacterCard.com Character (Direct Link or ID)
    Example: AICC/aicharcards/the-game-master
  • +
  • AICharacterCards.com Character (Direct Link or ID)
    Example: AICC/aicharcards/the-game-master
  • Direct PNG Link (refer to config.yaml for allowed hosts)
    Example: https://files.catbox.moe/notarealfile.png
  • RisuRealm Character (Direct Link)
    Example: https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712
  • From fd1fdc646665d733bd2179d9439391c3c26ce84d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:48:50 +0000 Subject: [PATCH 78/83] Fix fitting class resetting after picking BG --- public/scripts/backgrounds.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js index f83d4e044..683e9c4ee 100644 --- a/public/scripts/backgrounds.js +++ b/public/scripts/backgrounds.js @@ -482,10 +482,10 @@ function highlightNewBackground(bg) { */ function setFittingClass(fitting) { const backgrounds = $('#bg1, #bg_custom'); - backgrounds.toggleClass('cover', fitting === 'cover'); - backgrounds.toggleClass('contain', fitting === 'contain'); - backgrounds.toggleClass('stretch', fitting === 'stretch'); - backgrounds.toggleClass('center', fitting === 'center'); + for (const option of ['cover', 'contain', 'stretch', 'center']) { + backgrounds.toggleClass(option, option === fitting); + } + background_settings.fitting = fitting; } function onBackgroundFilterInput() { From a03193b2f7ee553cffdaf68d61f115d3d422518c Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 27 Jan 2025 11:08:08 -0800 Subject: [PATCH 79/83] Change docker create to run so it actually runs the container. --- .github/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/readme.md b/.github/readme.md index 28d3a390d..01a3ab8ba 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -274,7 +274,7 @@ You will need two mandatory directory mappings and a port mapping to allow Silly 1. Open your Command Line 2. Run the following command -`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'` +`docker run --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' -v '[extensions]':'/home/node/app/public/scripts/extensions/third-party':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'` > Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config. From 8b5e0df2d70b80d265a208cc021537c967e5a4d8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:56:15 +0200 Subject: [PATCH 80/83] Refactor reasoning placeholder clean-up --- public/script.js | 11 +++++++---- public/scripts/reasoning.js | 13 +++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index 38a4d562f..006f946d7 100644 --- a/public/script.js +++ b/public/script.js @@ -3194,10 +3194,6 @@ class StreamingProcessor { this.#checkDomElements(messageId); this.#updateMessageBlockVisibility(); const currentTime = new Date(); - // Don't waste time calculating token count for streaming - const tokenCountText = (this.reasoning || '') + processedText; - const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0; - const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); chat[messageId]['mes'] = processedText; chat[messageId]['gen_started'] = this.timeStarted; chat[messageId]['gen_finished'] = currentTime; @@ -3214,6 +3210,10 @@ class StreamingProcessor { } } + // Don't waste time calculating token count for streaming + const tokenCountText = (this.reasoning || '') + processedText; + const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0; + if (currentTokenCount) { chat[messageId]['extra']['token_count'] = currentTokenCount; if (this.messageTokenCounterDom instanceof HTMLElement) { @@ -3236,10 +3236,13 @@ class StreamingProcessor { if (this.messageTextDom instanceof HTMLElement) { this.messageTextDom.innerHTML = formattedText; } + + const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); if (this.messageTimerDom instanceof HTMLElement) { this.messageTimerDom.textContent = timePassed.timerValue; this.messageTimerDom.title = timePassed.timerTitle; } + this.setFirstSwipe(messageId); } diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 90721cd5d..c9b0efc3b 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -27,6 +27,7 @@ function getMessageFromJquery(element) { */ export class PromptReasoning { static REASONING_PLACEHOLDER = '\u200B'; + static REASONING_PLACEHOLDER_REGEX = new RegExp(`${PromptReasoning.REASONING_PLACEHOLDER}$`); constructor() { this.counter = 0; @@ -121,8 +122,8 @@ function registerReasoningSlashCommands() { callback: (_args, value) => { const messageId = !isNaN(Number(value)) ? Number(value) : chat.length - 1; const message = chat[messageId]; - const reasoning = message?.extra?.reasoning; - return reasoning !== PromptReasoning.REASONING_PLACEHOLDER ? reasoning : ''; + const reasoning = String(message?.extra?.reasoning ?? ''); + return reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); }, })); @@ -151,7 +152,7 @@ function registerReasoningSlashCommands() { return ''; } - message.extra.reasoning = String(value); + message.extra.reasoning = String(value ?? ''); await saveChatConditional(); closeMessageEditor('reasoning'); @@ -181,12 +182,12 @@ function setReasoningEventHandlers(){ return; } - const reasoning = message?.extra?.reasoning; + const reasoning = String(message?.extra?.reasoning ?? ''); const chatElement = document.getElementById('chat'); const textarea = document.createElement('textarea'); const reasoningBlock = messageBlock.find('.mes_reasoning'); textarea.classList.add('reasoning_edit_textarea'); - textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning; + textarea.value = reasoning.replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); $(textarea).insertBefore(reasoningBlock); if (!CSS.supports('field-sizing', 'content')) { @@ -277,7 +278,7 @@ function setReasoningEventHandlers(){ $(document).on('pointerup', '.mes_reasoning_copy', async function () { const { message } = getMessageFromJquery(this); - const reasoning = message?.extra?.reasoning; + const reasoning = String(message?.extra?.reasoning ?? '').replace(PromptReasoning.REASONING_PLACEHOLDER_REGEX, ''); if (!reasoning) { return; From abe240397d35d220820beac19d99d60580adb2ca Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:01:44 +0200 Subject: [PATCH 81/83] Set assistant role to bias in CC #3366 --- public/scripts/openai.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index d715377fb..2d0f44458 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1096,8 +1096,8 @@ async function preparePromptsForChatCompletion({ Scenario, charPersonality, name // Unordered prompts without marker { role: 'system', content: impersonationPrompt, identifier: 'impersonate' }, { role: 'system', content: quietPrompt, identifier: 'quietPrompt' }, - { role: 'system', content: bias, identifier: 'bias' }, { role: 'system', content: groupNudge, identifier: 'groupNudge' }, + { role: 'assistant', content: bias, identifier: 'bias' }, ]; // Tavern Extras - Summary From 145136059ea5c293f640cc1c4b2d6cc3017e6283 Mon Sep 17 00:00:00 2001 From: Small Eggs <144642298+small-eggs@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:43:24 -0800 Subject: [PATCH 82/83] Fix tts.skip_tags's regex to match newlines The extension_settings.tts.skip_tags setting is meant to skip sending tags and their content to the TTS API provider. The original regular expression matched content inside tags with ".*?". Unfortunately, Javascript's engine does *not* match newlines on the "." without the /s flag. The /s flag was added in ES2018. To be more compatible, the regex has been changed to "[\s\S]+?". This gives similar performance (instead of using capture groups) and matches all content within a tag, as the original regex intended. --- public/scripts/extensions/tts/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 4b8978422..279737f7a 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -466,7 +466,7 @@ async function processTtsQueue() { } if (extension_settings.tts.skip_tags) { - text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim(); + text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim(); } if (!extension_settings.tts.pass_asterisks) { From 0c9b301a57362df7c824cebf99cb3053fd40db85 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:39:09 +0000 Subject: [PATCH 83/83] Prevent ephemeral message extra changes vanish upon swiping --- public/script.js | 19 +++++++++++++++++++ public/scripts/slash-commands.js | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/public/script.js b/public/script.js index 006f946d7..48bf112a6 100644 --- a/public/script.js +++ b/public/script.js @@ -6074,6 +6074,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, return { type, getMessage }; } +export function syncCurrentSwipeInfoExtras() { + if (!chat.length) { + return; + } + const currentMessage = chat[chat.length - 1]; + if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') { + const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id]; + if (swipeInfo && typeof swipeInfo === 'object') { + swipeInfo.extra = structuredClone(currentMessage.extra); + } + } +} + function saveImageToMessage(img, mes) { if (mes && img.image) { if (!mes.extra || typeof mes.extra !== 'object') { @@ -8501,6 +8514,9 @@ function swipe_left() { // when we swipe left..but no generation. streamingProcessor.onStopStreaming(); } + // Make sure ad-hoc changes to extras are saved before swiping away + syncCurrentSwipeInfoExtras(); + const swipe_duration = 120; const swipe_range = '700px'; chat[chat.length - 1]['swipe_id']--; @@ -8636,6 +8652,9 @@ const swipe_right = () => { return unblockGeneration(); } + // Make sure ad-hoc changes to extras are saved before swiping away + syncCurrentSwipeInfoExtras(); + const swipe_duration = 200; const swipe_range = 700; //console.log(swipe_range); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 39b3db71e..531a8f51f 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -42,6 +42,7 @@ import { showMoreMessages, stopGeneration, substituteParams, + syncCurrentSwipeInfoExtras, system_avatar, system_message_types, this_chid, @@ -2814,8 +2815,11 @@ async function addSwipeCallback(args, value) { const newSwipeId = lastMessage.swipes.length - 1; if (isTrueBoolean(args.switch)) { + // Make sure ad-hoc changes to extras are saved before swiping away + syncCurrentSwipeInfoExtras(); lastMessage.swipe_id = newSwipeId; lastMessage.mes = lastMessage.swipes[newSwipeId]; + lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {}); } await saveChatConditional();