From ffd2621fc871e5f94d45a733b8376ad1e581be3b Mon Sep 17 00:00:00 2001 From: Jody Doolittle Date: Wed, 11 Jun 2025 02:56:31 -0700 Subject: [PATCH] Bring character schema up to v3 standards, add support v3 import support --- .prettierrc | 7 +- bun.lockb | Bin 153618 -> 155551 bytes drizzle/0004_fantastic_sumo.sql | 35 + drizzle/meta/0004_snapshot.json | 1575 +++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 4 +- .../characterForms/CharacterForm.svelte | 904 ++++++---- .../chatForms/CreateChatForm.svelte | 2 +- .../personaForms/PersonaForm.svelte | 3 +- .../sidebars/CharactersSidebar.svelte | 559 +++--- .../components/sidebars/ChatsSidebar.svelte | 8 +- .../sidebars/PersonasSidebar.svelte | 404 +++-- src/lib/server/connectionAdapters/ollama.ts | 772 ++++---- src/lib/server/db/schema.ts | 427 +++-- src/lib/server/sockets/characters.ts | 323 ++-- src/routes/chats/[id]/+page.svelte | 6 +- src/routes/images/[...reqPath]/+server.ts | 3 +- 17 files changed, 3606 insertions(+), 1433 deletions(-) create mode 100644 drizzle/0004_fantastic_sumo.sql create mode 100644 drizzle/meta/0004_snapshot.json diff --git a/.prettierrc b/.prettierrc index e8b0ee5..9979261 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,7 +3,6 @@ "singleQuote": false, "tabWidth": 4, "trailingComma": "none", - "printWidth": 100, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "overrides": [ { @@ -12,5 +11,9 @@ "parser": "svelte" } } - ] + ], + "printWidth": 80, + "useTabs": true, + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "ignore" } diff --git a/bun.lockb b/bun.lockb index da84e61129c496cb0753d06605670297c3a8be38..3c61404a42cccfef8b2cd97b48418bc86bcb59b6 100755 GIT binary patch delta 26460 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 delta 25446 zcmeHvcU)D+y7ro-TixhJL8XZe>+j9zR|rC z(&Cba#KG?c_@laQ1%;oC%oSkpmpKmsCG4G$9Gj5bH*KGrYUqqcp~4$LsqoL4nsM5Y zxWqxI#%vEY-4sx=a|100IvSKRO4am1pp-r?Ej5~I7MGUf8<))I6>`XkMHJPgJ1Et) z5h(sLqhki7Cd0H41XMS(K?O;22VDZ{3R+F0?JKJBLlUS!lGGc0w~4r0&PJhSqO%P%>$-(#SPHnkbeL*fi6M65#0bv_MOd=R1GvXuJ_=+ z(6u)6y9Pf8_xft`O(EQ?NVU$(Lex~=pj7W%cq;*lS=K)xHaaymE;<&2B4rKqlzU2Q z+@L`Taj7#QQ#*eSO6?bu8si%jC`rR0JMwWwyfe-=QpaGU#wuL^N|tXysoeSyQoGpR z^?Fb1J*oGY-jn(e(}$Kmbo3#kjf{+fA)$A@-sSpO)yJYfr1T-9k7<1j>tj?OYQd-g zHBr%Ub^h)}MQQA=1Eu+_RxBe{`ai1KKUXmtv;W(g>64*vTp|XHB)x2}*6cATb=lDP zgqV2fu~rVFbtHON!hkf;&K=a{sWm7qBacBdoWaCLr(v;{q-7n|Q5FkIOKDgq)uBz( z7wfF*`wmEng+J*n;uS}rWZxEcBp-$yHT_}eX??o_O8HzzhGaLwrWPbRExl-RO#g&I zVI6oaqhH2>ZmQjL4X%C9>md+djrVJDfw1kzq^Sj27jQnVImD+&vVe89W7o+)! zPKoZF7$+Ig8r1NvC3}WPcsh z12hj5?U#|+FJVvu$}3$1s6kuxQz;hzxHJ?d7&4W+3Mf_34U`IU0`}DK%@fsvPlZg& zLDwWTqtv*x0f|H6q(12UiqQ82bpUM(>Iixdc1Bv)FM}aP9%yOMS}E#CX|BnBpj0q@ zlk^>^j)YWXNHup$Q(O8Zs0lI(J}5mUPTH@@>p`hy=YrBcFa?zI>o^z{$^f$#4Ao>F zC>f5?5)K=pIvfm{YD~8^)TPC>_~Ap<0-pt?F3CrU$j{fqRQ;zKeE>bBzY0njpVsIB z&C7E!S`5x;5`R9~ zJJiqwTrfA)2sI7?R~v?fd8E5pURatt)UwD|L8@5fvSoM#q;_Ta#99{PSagWLlDSh2 zv%IkkckrYq248x*4tGwAb01`DHl6jjAIgr|lfnF?Vdc2Xj zaR)z(!HO{lSE$tlaP?rz`01)<<7r5gl2J<8-JLtsvB(|Vc?8H*cRms1xI53QV=*ql zLO{KNwnP`*^xzTx7GqVc5*>7BhM|yJag%qbaSu3Z<3ilY5B*t=M+8{pk39Lr0E;0J zD^3V+Q#sVQ5F9ldij7K~fkbVhHn&T8J~7Z@?0`wDX5duEEUzxl9fB;zYfw^O%SyL) zslX=&SqwX|z((*k9-+n}SV1BXEpev`W>F@uie|X?icoY@z^VSufKyX=RM%7V z0jKKLfNQIycvTSFs)p`w3OLp795~gVM@?PV8=P8#HQ>||{02@ffu)vi_c=J#?o5Hs z!$a05(PR69>!|3qgX^VmuIT>+h06pN zqi{FDMJrsh06lg!I5mYspd|HGVh4hYQ@C^Bq7<%fkj{MtPEGLyoLZ^~Oc~W~1vphF z*Oeq2rIq9A^1LP%IlC@*Xo`tmmq&mkhwzC_Eyh1WB&iDq0M;Tuvtfi;lKOFzPpJHh znMYK$7+o==)KQd$r799qFrOS8DzB)=^O{+V7onhmj_H7Of8cu?1qi~*0buOWR+fu+4 zLeZ>>U0ZPB3MUk9a=Wm?1*)3O^~lr$Zbt!E7>kdZv`qmw16)hR-Q@zVN=seWzku5S zuDN3OynqYC3askJ=$w406?cflz(PYnr?#>j70xF@xlmK0fvTD1XW`tTgT>&3@zRc) zI)oaAgKML3hYPqeR$Uiez^wsiRqXP?g)3YWbSbLVC9JT*kh`-5T=_P-S)T%KZ2|Y9 zfNP8`tfk^^d;xc|fOBc5$Myo(T(MhN!2Mdl)o!oH4%9j0esEY>XywP2Tr!eJbj2PK zsSl0QP0Yrnkgy!ku*N7ouSuABShyWJ@Q7{}V@wBqh{B6;86->?N>IaWlsl@!TFJqf z21%Qoh}#4S<4hfchCd)-<;0fS60<@ZoA5Udl4gZj=GqxnY8uSlUXc8ixjPvg<%;Qp zCE@!5E7}R&vC^n6?9%tUa)&63T(cXGh_V<4cax+B+|)GGa2;Gj zg`6;BaVZ7dX>d`B&c7Eey%amSfV&H>hoWo#vCb_3*GDQFOK39 zds~bTqa+E-07@R&&1|gmiQ1DiJ)bJxX>~%Fl4Hcs`KZYUT zI+I7^QE)V7it@-9v#~_)f;AJvqcbGxUv*)g0EwCl5_)@|CLx`N*^p0`v{)H@@tlKx z_X#!76OYnH^4=Ieu@CMyV^!xEJeAFIL@amcYmq0!@`%0`!}&O>Y@5EJhJZfQP{d6G z7o%{`z$GYL&%V^O6uS#t4~27!*X>4tQ`7zgu9p&PPM}I7g*-2TJM^;{PCyaCk5>$p zXZGV0p~&y272fFEUy?9~Fxaz#&BjrXXmx>v#yALx1{x&XLK_l!UVn>WR-$rOP%~7% znaCp&EylDYbxSEl16OuQ=6O(#OU6q=Bvm(qdyv9-n}ATc`T#yL$zn_#pbu!wNqOl2 z?vQLT{tRUcq%rU;>~URE)LDbA0c}4M62%pvd2YM{3ClvJr?UCB8Hi#kcV&~o+2_z< zNSJyk!|8Hnxq2FRNU<0$rO{SsN(nW(4pRH35ItfT+CWnDhVkH+#%Isl!idBWPxqj&;XL^dmOkX&@02pa2Qg3@*_(_l|nQes%JJC z99BkTaUWcy!ZjF1>)i3dVTHk{k&g3mUF0&4?WdPW5sC zTw|pig-0mm7&Ss!QDL+noLW`WNZq^>IIPBqomHSKJWAJv7jRR+sRcO-PRYPfVYF@+ z15S-y08VLG<4thXn@ZCgYi6iZTb(4IW$=l^EplGQ#=PO5vW#+)B%xBuMN|e*R(d3s zz5p079-xb8VPFyoxQH5nET9nZ6+jnJO7}GhxZb6tp8*gRc)|uz7{$SC@Kk`gq{Q_h zO7Zgm$}k(CizqoQcVgpN^C7nCYnJGd^N>iQ8MwA6!vvtPf0?wbVQvXx71|N z3}qCCLqWO-E#h5DDyyb{m(s%19(o$Z9W>fWqg^z6qSUpCn*2YhHvj%-1XBHF!v9cw z#y>2X+9F9>{JWHDJOFxEQ2M+>*S|_L{?&y4iF8G1{hy@O;9W|p$(o)h4bo|vOq53V zR~n@cT@*Eo0*w+oTa$@8KwhlL?^0Q+gUtaE6t_$>AW9jo1f{0_R?`z@yv(joYxQqlK6a-FC72hC6vN1>qN9@h*`Xa?_6a&%VH zze`C)pDF2jmy+rn0-DU1LCODB&CX6Shl(TOt`_lsL20r-(hBfI%cp>rd0#<>y!<%V z465%NNYrqQ3jZFZgtDf8mr_R-(ey-#Erx(r*HWOA&WV&7rJdDVi}%su@zV@-aiQO6 z&_$FI`fAhQ zMnp{4=p0Qy7nBlaYjmlmU#8LJpmZ&lH~u-i!{7I+YHR$}Jt|pHJarS@qtZo`A_xx3YG8;Cj?=t%?5t zDUw&-fcilix53If^3#yw*O_?TjaJr~kJ*U&tvB&sAlZ1pCe#no*PE=Y8@~=|*aj1C zzS+up@T|?K-$oPv9a1mecnj(WX~`BVi{eipjo*a&fN&VUjxe3q z-)m#Tc^1ME{0_pAyzxF88^vcK9L=8~%;4esZ7h>#BOJr?5q`=e57^jPz5?NAoPB3w z<9IiOpY!zyzu?A$Ha4C|Bb>muA)Lsa4%yh3JOSY(z7OGK?ta+DrtpCXr}A8c(|F}0 zHkQTH5l-i)5prHL*T%l$V-SALFCd)31M+N4@CgWK^6Lm^@%l$?Y&OqAIEUXsIF~m* zW@F#*SqSIxCkW^B@Z&bNfM+Aj=J^O0^2if5wurAlxR^&Twy`CgowTu~yc@!0eEmuE z!*PtWQ&zTuN1sAJKspF%6?Zy~emH?~cG}8T^L>!Ip2RpiV`Xdkz%v+Ur!dYSt>=}$ z$2fyD?t3fS$WKFxKaFvA*2*^XF=sK(&S0ED%HaV&V4Okv`Uh(p{q;QUDTVt~O>xaK zo(k%A)W6o(S6RdE8>+Z`Ua!l_=k?Pc{WL$hC4TquW$lN>e%s@9{-O7!^kVm3=YJWV zS1#er;MsA9s=WH0Cw=dhG5y!FUO`I(jz^ujn19JR zIWxV*h_#H^&e=zYjKUKI=!! zpK~Vu8qxtCejf7&((3b8c97?vx3NP!@`8;W<|_~$;p`_H%jMk==JE9ik8!W`)wP0$_FBR#&Z$=#w*{kvEO+*!aw+FgwJ`+yEgWMk3pEvFChGr2i&vC zFC{+lUKd{DE|&3oR{6EWKf2$ApNII(eXIOd;&&mAzlY9vU}cQYdVtQjkIsNph=)H! zXFyv0(8>%vAJWVRCf@6jl@;YH9$^waG;z~oD>L$Lk1>fL?SfRC8=qhjJ;Flw#L7&3 z8>Fs}vCuuWvXVUEDJIbqEOd~Zxcf6qB1og2Sy^eG3n~67HmcvOtSnFe4U^~@HY!N2 zyyovV{O)lKLU(=vp$8B6!^X<-2?#y;b%f=4{pU7TfoCDC$nPMm#2de`vC4cFLNERV zp*IiDx3MZb8(~$RpN|^7K#l&ivg&-rpEg#5vzInjlXrWG`sAZNFKKf%zOu2}JQ|@d z-}VZ%d5PM*rmZ#MHEQz;wSg4C-QS=#kVd_stu+@?{A<+aEp4soZ*44upGIi@x+b%+ zdS8!0SpVw_2tN`53?x)cAQC376KNpo%ODn!MWms)L!^;tTnMDGm_?+CctWJ92rtZR z;rQxPb_MYkeQydcFcQdottC=|;LP$U(F zqK()_impYVC|?YUb|Rq|6vc`{ahw#9!rchPR#J>ILeWv=k|Mqs6uu5nbQb9jP`DYP zxJ(M0s97Ar5mHPp4n;R{ffU0WplDD6iXLJ@2`Fk5hvES#dWrfbD9)4O8xs^!;tnas zmw=*;BNWkMmLnAPOi;WgMT`h93B_$vtS$*foX97|Oh+hsm4c$LSWya!Rwbb@IYE&i zx;a7df)u+*(O(#yp;%rDiX>+!lEgMrbajHFd}$~Kh=kHm6my2+I4K4S_cBmyCB>*R zP^5`mQpA^r!nZ6GgGG8-DBQ|GahViDMNJndj*w!q3l!<%0x5=-g`$Bg6eGk0S14+@ zK=FVSqeOi-D9)4O8#gF2#2r$McZH&jI}~HYEO#jCxiM4DYj?J>5O3sOSZ+}=r(Xqj zi1AYHWk?}{E3rg_AHMLIa@KpZ9gLI?`hSxxE-#Y3SU<(^u@@^Quc|24cw^_Ll+(Of zF_)hztG_L$7Y_RW@s#(P0}RZr?q|Kx<1+r4uf1KAWNPe~B- zg4jofNBE_U1C>oFM=|~X9fYM4^H)ln5{w2ao=kQBQ}OtiiD?zs+%5Ilvv7w-+6TC< znd0eK)+9rE^H#lRm(^eOR3HJDE>o*ML)*|ruYriAH}D4m;^~zSeVn70>-fN=T)j0r zdKKia>0&e;y^P;N2_>{%EErPIoAgLUiJ!h|hV)v096(opO-C=*n<^T7=M+yqW2M_B zYfaz!@kF(1)9{fD7OXxB-;``jA}_ zr~uGco_oN3fW9m}0M-I@XVeAdqtAm=fT=)RCiRorfoTo20jMLWgQ(-^rx~k&Z-KSI zI$%Ap5!eK5266yD*wz93fdIe($c6qea0oa8qyxi6(ihA-gT7rX1Qr2{fh7P3z5-?d zvw&&9RA4Od8PEryuQbhp7C=>i9@&k61CWm_UjnZP$m%)p2Vj7l4_XxTHRv1qe)=bv zmjLx{A%FogP#7o%7=agvehU-PfWYP>;N_b^!0NZkOfQ!I1mZ62L=GcfH6QiKm%+L@F_4HNC8FwgMqQY zIAA33888YM2n+#61E~Pb+@W0&qyb+7nHUmpe^eJ^{J_ z(LfI%0idO`H_#7=0c=1|AQqqj6$kVJx&f!Ox78_Gv<`Y@&}bb4WCAn-$#*Lt8--X1 zv;f}-C<~MUN&{}tJA*m_ufXpCXmIWXb^!F)NlVXGU<=R@pk-+RFdv|f?gMn7O&!}K z{h(CBCN9+yag*3RF=jIEnWjxdM@<_`rBm!TLnfDi^yBsplJ@r^-gr)mjMp{W0w z1K~hBfSQS_9u0H>x&fa6QNYJQF91C(^#Hm9T>%@=8R!IbA&-=Z5`O~30=#1APH%(As6mg0yr$kPMmxB#Nx5tZB_*V21)jfWg2ZAPq<03I7r;xP6jSy;WgjdF-UPgb-UXm7uP`7362O2$Kyjc1K=H)@ z(ia7a&~IqSfQ$_Q?fGOthLngXj1)?V903}w^8tVGE&#QDP0%)=H9)Ham4J#s1)x0O z36ujo0C&I*pc@}=+DEH`sRDQb)qu*tyE<>k1d7x7I^gMsg*J8C(0u^f+({-h1gMa~ zpfpSpfdKH)ph2L402LU|v7D98s1LCoU0Sy9esmZim(drxy zOn^QcXb-*}KqIs*Xaqo0sSRjrzzR@%(>#d;O`zq23@D*aJAfg}KXsc0t<+ zZ3(m$5G5V$HG149;AzVt^ajYD;#7w!-$&C^`o3DcZbww#hGH``gM@dC^i0X2o@oqt z(!ZMt6<9B<9#68pcJ}mo+V}ls=$YuwbVu)Iq-R39!GP|BYDxCgdV>JUBn_Z=g6^Br zf5>k-O~PS_)H9$&Bs^+M+6xf02Z@G#7l1~(U=k-Nt$tR~lizEv{Jd~> zK3SL*|Gy$_Nu=F%Y;%iZvs(19d;P;CJQty4Mc|s6xyQOlyODsbjfZAb6brkJk z5#j@jGO%dgyw#+UWeUfu`3Itp8X~4FVltz8PA$6VR8@+hdmgMXj$+;-7U-D(3oP%M zhQ1N~uNL>YP8R45Tw@%?eae3pEZku+anH6=7cbo$M;5_~#Rf-FX)%20Cot_?xW{J( z|0N8qkEW**&MPUB7sH2sY?0^lac6oOUk!%^ibvhPwWL@`KJ-(Xb~{A&^75(k8W#S3 zb;;sFN%43w>m%PODY`CUf%3DGV$u@kCO0f4mM&qfJ@s>)tY3eA={Wo0pyC@XL?=H9 z$E8$0C$;CQv&AijZ?-&zh1P-{okUMq$WcyW8d>URLG3tmtM~G>O#4#<-l@?zuL%BYfE2a(}2+D|8%aE&FR`@T6&-rCV z#B!w2PlJl+ljhpJNAyGZ^!E#)6g6DLhUG}H&rRGSOZ|)`sn7239!wi}36_C=L1gLX zAr`EJ&ma%+_$VtYMy+7wXtCCO{fS1eK#$`wKIFzreaw#c^Kha@fIg%Ez<3(=vO^94|2vX+8CmhUS&? z-~RobnC^Cq@4Uo1^8Tad!{plR&b&244%;mrAcoe3{nEph)$YiR>@mz+yrH!E$yFD} z_Gu9?@ioph37hyOh1i@hu-(@H+lAWyM?ieXb+1J{oJM_e`Ho1xwtd8Tj&Qe6?3xq zY-L{^v&S^4BIY42&8)5KFdi2Ai2IZq9g2nBG;{OY`%fJ_@~ReQ!G6KYnsLELR9cVR z^kcNn1@3q|`KgWBE%bx7iVQjvn|R`xY>x@7Es`m%el%CT$0OP~mipqf-9kUS>&vOH z=jM1j-L=O|uPt^Wt*3t6*R94?i~ZiT*>`q}?`w<4*U!Q z`!d}KG&^@-*3Wy~Xq^jI*GSc|o7~wfA~v&4;*)IV;rG6yGHhHu^{_SlaI=wF`??q1 zTj?8_6|XO9Z(*HeEIprYVXfunmBpzoteH_S)B{mIhXr{~4O6G>D;K%*^4{Mi+iwXL zg^9!*)M6_vXj_l@HT}55f;sP491jx@OZUBAEp z>1|`tY&&)`hQia0TEe!l<-tXgZqd>aP|q*WU(!!)`*p^kr`Zkv*kO;sg7ocnw3~j; zTS)6;Te7Eb8g941GJg#gAs=ff2sJI1`E}&|LyNg;@u7uSjVV_}yrHxo+TK))4{eQV%#U88^-h#bKb&t> zi-caO4Xe+!mwf~_AhP(-7NzDun(2AAKFCK zlCAX;JCWA&gMFx^MISyU?;q?##o|LZ532X}-omm6cW#f{i@kd=LtnNRKkdPW9M(ZN z?nSQp$$wL4wTypSZe<y|qwtfTxwiox~?(p`QlmHh;vd z$;&$hY8JRV)LcLAB-X%CcPYQ^ByQ}ZT67XF`>~qn#}Lk_dL(YJbH@XU1Lg5SKX`D% z+9LdHK#MQ!uHkd^e)!a~P|H%*P>`wkc|Y^eW3{@-BWxo001IR{MAl*E?x`RDmssa+IM4Pwjqd|93;XCxZerRF*k}g}a+AmG9Tj<9chE=Niaf{mv>)K;xb{C0A z>!}}vIR0LzrJI(v#%-i^Z($YpGO^p(}Dee2-(W>;Ys(4pl6(lTL z=(yF{nRXy8rhk+uaR@gQBceoiqWRHc`5|^&>rxp{GT+kki1wgX$gDk}gy=^+#y{Hh zs6+6en($bcwyXf@A3tCkV};)lw8B5=eUvjVR*XD?mb@OT9`Kk^xP$kLtns*Y4e$%X z!iSgIv0@!8Li8gcM^D)O^Uqsfq}wg@LnCvG_>3!_c@j6(dfL`;;ti$MkC}A$J=1ad z_Bq?_7US{lgGQfke04*&$%Y^8F-zh^>s*vfKe{q5{=(e}KVLa(w{YtxaO|vGhu2^?J1`nlcy*#cudC!r+(rD(w2GurBx{w)ldAH%PPru ziQt*X-1_Oi+&GhcH_+>5Km(;G@c^%WKz;XxS1Hy7@3EVrHu~X{g+AK2KCIu2D@YSq z57R9`(vPS7vTE|2PU~XtAtnUV4Q1_`B^zE2IjMaKqR*LF7P~Dh2 ze;)1FZ_CE9ntKc$)m@h%f10`G(VbP_*<&W9irOboGX40?{1 zsg*sger{+-r;cTY^g7bU9#cF`yh2*pEls$bMBnyF6W7i{9-JoD{Qx>9O^iMXIweia z`;lspraqx`_sPEZRjqANG|~c;`LjMvH2NNv`_n{wSjs2UW}JdUg}BLDtfz7$D(V*< zd2#NIpGG&N^hzb9_tJanXQi%aSN`1TG>%e;25ICTm+Xw*|bMfLOavoGsr?l~$m zyfIS-j9wPq_xrWe^}~mX1*hLHyPJG=sCYd)tdQVbe3Ln&4Hd!6SY`TWg8~+opkjj>p#fk zzxTIW!2tRvi~P?Q^}RCwFN@Q3gxGuG16`^2yzX5e=lVgo6VH?iYI=0leS4Q_ZPb5+ zxKG1eucuD+a_O?((s~B}k1o>>*3FyYwtEG;(cPZ!(~)8l)j)TsInwjdqxJmt3hVjm zc6xHH;-3GwqIwPAE3xL-Q$Omr?8>NImukD8+e@lvskiO>S!->qFAaK$^pfkz-%s{l zIo>zZqBU>0MG}{ok8?kK55?Q-OzB5_al+N;M{)KNYbIPSvp|n-cpXpzk)PG8dyEZp ze3POX4Y%d=zRc#ciphO_W8w!V_xBwXH*8RfE~WWGN{x;gG~+6M8a(VOv*hf#%32i9 zu{~g;iijfjSxC;=XKY0AoOy4V%}rE)zyfl9bCtgqyGZW|9Fr9 diff --git a/drizzle/0004_fantastic_sumo.sql b/drizzle/0004_fantastic_sumo.sql new file mode 100644 index 0000000..f612d94 --- /dev/null +++ b/drizzle/0004_fantastic_sumo.sql @@ -0,0 +1,35 @@ +PRAGMA foreign_keys=OFF; +--> statement-breakpoint +CREATE TABLE `__new_characters` ( + `id` integer PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `name` text NOT NULL, + `nickname` text, + `character_version` text DEFAULT '1.0', + `description` text, + `personality` text, + `scenario` text, + `first_message` text, + `alternate_greetings` text, + `example_dialogues` text, + `metadata` text, + `avatar` text, + `creator_notes` text, + `creator_notes_multilingual` text, + `group_only_greetings` text DEFAULT '[]', + `post_history_instructions` text, + `source` text DEFAULT '[]', + `assets` text DEFAULT '[]', + `created_at` text DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text, + `lorebook_id` integer, + `extensions` text DEFAULT '[]', + `is_favorite` integer DEFAULT false, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`lorebook_id`) REFERENCES `lorebooks`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +INSERT INTO `__new_characters`("id", "user_id", "name", "description", "personality", "scenario", "first_message", "example_dialogues", "metadata", "avatar", "created_at", "updated_at", "lorebook_id", "is_favorite") SELECT "id", "user_id", "name", "description", "personality", "scenario", "first_message", "example_dialogues", "metadata", "avatar", "created_at", "updated_at", "lorebook_id", "is_favorite" FROM `characters`;--> statement-breakpoint +DROP TABLE `characters`;--> statement-breakpoint +ALTER TABLE `__new_characters` RENAME TO `characters`;--> statement-breakpoint +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..d2f7ea9 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,1575 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d5ac601c-8fcf-48fb-b35e-89a30184d9e3", + "prevId": "bf728d98-9a55-4e67-8625-f7a408e59d45", + "tables": { + "character_tags": { + "name": "character_tags", + "columns": { + "character_id": { + "name": "character_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_tags_character_id_characters_id_fk": { + "name": "character_tags_character_id_characters_id_fk", + "tableFrom": "character_tags", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "character_tags_tag_id_tags_id_fk": { + "name": "character_tags_tag_id_tags_id_fk", + "tableFrom": "character_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nickname": { + "name": "nickname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "character_version": { + "name": "character_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'1.0'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario": { + "name": "scenario", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_message": { + "name": "first_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "alternate_greetings": { + "name": "alternate_greetings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "example_dialogues": { + "name": "example_dialogues", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_notes": { + "name": "creator_notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_notes_multilingual": { + "name": "creator_notes_multilingual", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_only_greetings": { + "name": "group_only_greetings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "post_history_instructions": { + "name": "post_history_instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "assets": { + "name": "assets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lorebook_id": { + "name": "lorebook_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extensions": { + "name": "extensions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "is_favorite": { + "name": "is_favorite", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "characters_user_id_users_id_fk": { + "name": "characters_user_id_users_id_fk", + "tableFrom": "characters", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "characters_lorebook_id_lorebooks_id_fk": { + "name": "characters_lorebook_id_lorebooks_id_fk", + "tableFrom": "characters", + "tableTo": "lorebooks", + "columnsFrom": [ + "lorebook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_characters": { + "name": "chat_characters", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_characters_chat_id_chats_id_fk": { + "name": "chat_characters_chat_id_chats_id_fk", + "tableFrom": "chat_characters", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_characters_character_id_characters_id_fk": { + "name": "chat_characters_character_id_characters_id_fk", + "tableFrom": "chat_characters", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "persona_id": { + "name": "persona_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_edited": { + "name": "is_edited", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_generating": { + "name": "is_generating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "adapter_id": { + "name": "adapter_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_hidden": { + "name": "is_hidden", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_messages_chat_id_chats_id_fk": { + "name": "chat_messages_chat_id_chats_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_user_id_users_id_fk": { + "name": "chat_messages_user_id_users_id_fk", + "tableFrom": "chat_messages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_character_id_characters_id_fk": { + "name": "chat_messages_character_id_characters_id_fk", + "tableFrom": "chat_messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_messages_persona_id_personas_id_fk": { + "name": "chat_messages_persona_id_personas_id_fk", + "tableFrom": "chat_messages", + "tableTo": "personas", + "columnsFrom": [ + "persona_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_personas": { + "name": "chat_personas", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "persona_id": { + "name": "persona_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "group_reply_strategy": { + "name": "group_reply_strategy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ordered'" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_personas_chat_id_chats_id_fk": { + "name": "chat_personas_chat_id_chats_id_fk", + "tableFrom": "chat_personas", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_personas_persona_id_personas_id_fk": { + "name": "chat_personas_persona_id_personas_id_fk", + "tableFrom": "chat_personas", + "tableTo": "personas", + "columnsFrom": [ + "persona_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_group": { + "name": "is_group", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "chats_user_id_users_id_fk": { + "name": "chats_user_id_users_id_fk", + "tableFrom": "chats", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "connections": { + "name": "connections", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra_json": { + "name": "extra_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_counter": { + "name": "token_counter", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'estimate'" + }, + "prompt_format": { + "name": "prompt_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'vicuna'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "context_configs": { + "name": "context_configs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_immutable": { + "name": "is_immutable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "always_force_name": { + "name": "always_force_name", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lorebook_entries": { + "name": "lorebook_entries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "lorebook_id": { + "name": "lorebook_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_secondary": { + "name": "key_secondary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "constant": { + "name": "constant", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vectorized": { + "name": "vectorized", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selective": { + "name": "selective", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selective_logic": { + "name": "selective_logic", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "add_memo": { + "name": "add_memo", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable": { + "name": "disable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "exclude_recursion": { + "name": "exclude_recursion", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prevent_recursion": { + "name": "prevent_recursion", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delay_until_recursion": { + "name": "delay_until_recursion", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "probability": { + "name": "probability", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_probability": { + "name": "use_probability", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_override": { + "name": "group_override", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_weight": { + "name": "group_weight", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scan_depth": { + "name": "scan_depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "case_sensitive": { + "name": "case_sensitive", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "match_whole_words": { + "name": "match_whole_words", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_group_scoring": { + "name": "use_group_scoring", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sticky": { + "name": "sticky", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cooldown": { + "name": "cooldown", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delay": { + "name": "delay", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_index": { + "name": "display_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lorebook_entries_lorebook_id_lorebooks_id_fk": { + "name": "lorebook_entries_lorebook_id_lorebooks_id_fk", + "tableFrom": "lorebook_entries", + "tableTo": "lorebooks", + "columnsFrom": [ + "lorebook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lorebooks": { + "name": "lorebooks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entries": { + "name": "entries", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "lorebooks_user_id_users_id_fk": { + "name": "lorebooks_user_id_users_id_fk", + "tableFrom": "lorebooks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personas": { + "name": "personas", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "connections": { + "name": "connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "personas_user_id_users_id_fk": { + "name": "personas_user_id_users_id_fk", + "tableFrom": "personas", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompt_configs": { + "name": "prompt_configs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_immutable": { + "name": "is_immutable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sampling_configs": { + "name": "sampling_configs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_immutable": { + "name": "is_immutable", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "temperature": { + "name": "temperature", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0.7 + }, + "temperature_enabled": { + "name": "temperature_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "top_p": { + "name": "top_p", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0.92 + }, + "top_p_enabled": { + "name": "top_p_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "top_k": { + "name": "top_k", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 80 + }, + "top_k_enabled": { + "name": "top_k_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "repetition_penalty": { + "name": "repetition_penalty", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1.15 + }, + "repetition_penalty_enabled": { + "name": "repetition_penalty_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "frequency_penalty": { + "name": "frequency_penalty", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0.2 + }, + "frequency_penalty_enabled": { + "name": "frequency_penalty_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "presence_penalty": { + "name": "presence_penalty", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0.6 + }, + "presence_penalty_enabled": { + "name": "presence_penalty_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "response_tokens": { + "name": "response_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 512 + }, + "response_tokens_enabled": { + "name": "response_tokens_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "response_tokens_unlocked": { + "name": "response_tokens_unlocked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "context_tokens": { + "name": "context_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 4096 + }, + "context_tokens_enabled": { + "name": "context_tokens_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "context_tokens_unlocked": { + "name": "context_tokens_unlocked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "seed": { + "name": "seed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": -1 + }, + "seed_enabled": { + "name": "seed_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_connection_id": { + "name": "active_connection_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_sampling_id": { + "name": "active_sampling_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_context_config_id": { + "name": "active_context_config_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_prompt_config_id": { + "name": "active_prompt_config_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "users_active_connection_id_connections_id_fk": { + "name": "users_active_connection_id_connections_id_fk", + "tableFrom": "users", + "tableTo": "connections", + "columnsFrom": [ + "active_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_active_sampling_id_sampling_configs_id_fk": { + "name": "users_active_sampling_id_sampling_configs_id_fk", + "tableFrom": "users", + "tableTo": "sampling_configs", + "columnsFrom": [ + "active_sampling_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_active_context_config_id_context_configs_id_fk": { + "name": "users_active_context_config_id_context_configs_id_fk", + "tableFrom": "users", + "tableTo": "context_configs", + "columnsFrom": [ + "active_context_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_active_prompt_config_id_prompt_configs_id_fk": { + "name": "users_active_prompt_config_id_prompt_configs_id_fk", + "tableFrom": "users", + "tableTo": "prompt_configs", + "columnsFrom": [ + "active_prompt_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d3c104f..4c45cb9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1749526176881, "tag": "0003_magical_pride", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1749630355164, + "tag": "0004_fantastic_sumo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index cdb78a3..6a70443 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,6 @@ "drizzle-kit": "^0.30.2", "env-paths": "^3.0.0", "js-yaml": "^4.1.0", - "png-chunk-text": "^1.0.0", - "png-chunks-extract": "^1.0.0", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.11", @@ -50,12 +48,14 @@ "vite": "^6.2.6" }, "dependencies": { + "@lenml/char-card-reader": "^1.0.6", "@lucide/svelte": "^0.511.0", "@types/lodash": "^4.17.17", "@types/pngjs": "^6.0.5", "better-sqlite3": "^11.8.0", "dotenv": "^16.5.0", "drizzle-orm": "^0.40.0", + "file-type": "^21.0.0", "gpt-tokenizer": "^3.0.0", "handlebars": "^4.7.8", "llama-tokenizer-js": "^1.2.2", diff --git a/src/lib/client/components/characterForms/CharacterForm.svelte b/src/lib/client/components/characterForms/CharacterForm.svelte index e11773e..c0a086e 100644 --- a/src/lib/client/components/characterForms/CharacterForm.svelte +++ b/src/lib/client/components/characterForms/CharacterForm.svelte @@ -1,322 +1,628 @@ -
-

- {mode === "edit" ? `Edit: ${character.name}` : "Create Character"} -

-
- - -
-
-
- - - - - -
-
- -
- -
-
-
- - -
-
- - {#if expanded.description} - - {/if} -
-
- - {#if expanded.personality} - - {/if} -
-
- - {#if expanded.scenario} - - {/if} -
-
- - {#if expanded.firstMessage} - - {/if} -
-
- - {#if expanded.exampleDialogues} - - {/if} -
-
+
+

+ {mode === "edit" ? `Edit: ${character.nickname || character.name}` : "Create Character"} +

+
+ + +
+
+
+ + + + + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + {#if expanded.description} + + {/if} +
+
+ + {#if expanded.personality} + + {/if} +
+
+ + {#if expanded.scenario} + + {/if} +
+
+ + {#if expanded.firstMessage} + + {/if} +
+
+ + {#if expanded.alternateGreetings} +
+ {#each editCharacterData.alternateGreetings as greeting, idx (idx)} +
+ + +
+ {/each} + +
+ {/if} +
+
+ + {#if expanded.creatorNotes} + + {/if} +
+
+ + {#if expanded.creatorNotesMultilingual} +
+ {#each Object.entries(editCharacterData.creatorNotesMultilingual) as [lang, note], idx (lang)} +
+ + + +
+ {/each} +
+ + + +
+
+ {/if} +
+
+ + {#if expanded.groupOnlyGreetings} +
+ {#each editCharacterData.groupOnlyGreetings as greeting, idx (idx)} +
+ + +
+ {/each} + +
+ {/if} +
+
+ + {#if expanded.postHistoryInstructions} + + {/if} +
+
+ (editCharacterData.isFavorite = e.checked)} /> + Favorite +
+
\ No newline at end of file + open={showCancelModal} + onOpenChange={handleCancelModalOnOpenChange} + onConfirm={handleCancelModalDiscard} + onCancel={handleCancelModalCancel} +/> diff --git a/src/lib/client/components/chatForms/CreateChatForm.svelte b/src/lib/client/components/chatForms/CreateChatForm.svelte index fc7eda8..6a4268c 100644 --- a/src/lib/client/components/chatForms/CreateChatForm.svelte +++ b/src/lib/client/components/chatForms/CreateChatForm.svelte @@ -118,7 +118,7 @@ bind:value={selectedCharacterId} > {#each characters as c} - + {/each}
diff --git a/src/lib/client/components/personaForms/PersonaForm.svelte b/src/lib/client/components/personaForms/PersonaForm.svelte index 3297d96..2f0b7bb 100644 --- a/src/lib/client/components/personaForms/PersonaForm.svelte +++ b/src/lib/client/components/personaForms/PersonaForm.svelte @@ -179,6 +179,7 @@ name={editPersonaData.name ?? (mode === "edit" ? "Edit Character" : "New Character")} background="preset-filled-primary-500" + imageClasses="object-cover" > @@ -217,7 +218,7 @@ - - - -
- -
-
- {#if filteredCharacters.length === 0} -
No characters found.
- {:else} - - {#each filteredCharacters as c} - -
handleCharacterClick(c)} - > - - #{c.id} - - - - -
-
{c.name ?? "Unnamed"}
- {#if c.description} -
- {c.description} -
- {/if} -
-
- - -
-
- {/each} - {/if} -
- {/if} + {#if isCreating} + + {:else if characterId} + + {:else} +
+ + + +
+
+ +
+
+ {#if filteredCharacters.length === 0} +
+ No characters found. +
+ {:else} + + {#each filteredCharacters as c} + +
handleCharacterClick(c)} + > + + #{c.id} + + + + +
+
+ {c.nickname || c.name} +
+ {#if c.description} +
+ {c.description} +
+ {/if} +
+
+ + +
+
+ {/each} + {/if} +
+ {/if} (showDeleteModal = e.open)} - contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" - backdropClasses="backdrop-blur-sm" + open={showDeleteModal} + onOpenChange={(e) => (showDeleteModal = e.open)} + contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" + backdropClasses="backdrop-blur-sm" > - {#snippet content()} -
-

Delete Character?

-

- Are you sure you want to delete this character? This action cannot be undone. -

-
- - -
-
- {/snippet} + {#snippet content()} +
+

Delete Character?

+

+ Are you sure you want to delete this character? This action + cannot be undone. +

+
+ + +
+
+ {/snippet}
(showImportModal = e.open)} - contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" - backdropClasses="backdrop-blur-sm" + open={showImportModal} + onOpenChange={(e) => (showImportModal = e.open)} + contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" + backdropClasses="backdrop-blur-sm" > - {#snippet content()} -
-

Import Character

-

- Import your character card or JSON file here. Make sure the file is in the correct - format. -

- -
- -
-
- {/snippet} + {#snippet content()} +
+

Import Character

+

+ Import your character card or JSON file here. Make sure the file + is in the correct format. +

+ +
+ +
+
+ {/snippet}
+ open={showUnsavedChangesModal} + onOpenChange={handleUnsavedChangesOnOpenChange} + onConfirm={handleCloseModalDiscard} + onCancel={handleCloseModalCancel} +/> \ No newline at end of file diff --git a/src/lib/client/components/sidebars/ChatsSidebar.svelte b/src/lib/client/components/sidebars/ChatsSidebar.svelte index a519afa..dea37f2 100644 --- a/src/lib/client/components/sidebars/ChatsSidebar.svelte +++ b/src/lib/client/components/sidebars/ChatsSidebar.svelte @@ -142,7 +142,8 @@ @@ -155,6 +156,7 @@ src={cp.persona.avatar || ""} size="w-[4em] h-[4em]" name={cp.persona.name} + imageClasses="object-cover" > @@ -183,7 +185,7 @@
-
-
- -
-
- {#if filteredPersonas.length === 0} -
No personas found.
- {:else} - {#each filteredPersonas as p} - - -
handlePersonaClick(p)} - > - - #{p.id} - - - - -
-
{p.name ?? "Unnamed"}
- {#if p.description} -
- {p.description} -
- {/if} -
-
- - -
-
- {/each} - {/if} -
- {/if} + {#if isCreating} + + {:else if personaId} + + {:else} +
+ +
+
+ +
+
+ {#if filteredPersonas.length === 0} +
+ No personas found. +
+ {:else} + {#each filteredPersonas as p} + + +
handlePersonaClick(p)} + > + + #{p.id} + + + + +
+
{p.name ?? "Unnamed"}
+ {#if p.description} +
+ {p.description} +
+ {/if} +
+
+ + +
+
+ {/each} + {/if} +
+ {/if} (showDeleteModal = e.open)} - contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" - backdropClasses="backdrop-blur-sm" + open={showDeleteModal} + onOpenChange={(e) => (showDeleteModal = e.open)} + contentBase="card bg-surface-100-900 p-4 space-y-4 shadow-xl max-w-screen-sm" + backdropClasses="backdrop-blur-sm" > - {#snippet content()} -
-

Delete Persona?

-

- Are you sure you want to delete this character? This action cannot be undone. -

-
- - -
-
- {/snippet} + {#snippet content()} +
+

Delete Persona?

+

+ Are you sure you want to delete this character? This action + cannot be undone. +

+
+ + +
+
+ {/snippet}
diff --git a/src/lib/server/connectionAdapters/ollama.ts b/src/lib/server/connectionAdapters/ollama.ts index 24d73bc..eb314d6 100644 --- a/src/lib/server/connectionAdapters/ollama.ts +++ b/src/lib/server/connectionAdapters/ollama.ts @@ -8,377 +8,441 @@ import { TokenCounterOptions } from "$lib/shared/constants/TokenCounters" import { TokenCounters } from "../utils/TokenCounterManager" export class OllamaAdapter { - connection: SelectConnection - sampling: SelectSamplingConfig - contextConfig: SelectContextConfig - promptConfig: SelectPromptConfig - chat: SelectChat & { - chatCharacters?: SelectChatCharacter & { character: SelectCharacter }[] - chatPersonas?: SelectChatPersona & { persona: SelectPersona }[] - chatMessages: SelectChatMessage[] - } + connection: SelectConnection + sampling: SelectSamplingConfig + contextConfig: SelectContextConfig + promptConfig: SelectPromptConfig + chat: SelectChat & { + chatCharacters?: SelectChatCharacter & { character: SelectCharacter }[] + chatPersonas?: SelectChatPersona & { persona: SelectPersona }[] + chatMessages: SelectChatMessage[] + } - private _client?: Ollama; - private _tokenCounter?: TokenCounters; + private _client?: Ollama + private _tokenCounter?: TokenCounters - constructor({ - connection, - sampling, - contextConfig, - promptConfig, - chat - }: { - connection: SelectConnection - sampling: SelectSamplingConfig - contextConfig: SelectContextConfig - promptConfig: SelectPromptConfig - chat: SelectChat & { - chatCharacters?: SelectChatCharacter & { character: SelectCharacter }[] - chatPersonas?: SelectChatPersona & { persona: SelectPersona }[] - chatMessages: SelectChatMessage[] - } - }) { - this.connection = connection - this.sampling = sampling - this.contextConfig = contextConfig - this.promptConfig = promptConfig - this.chat = chat - } + constructor({ + connection, + sampling, + contextConfig, + promptConfig, + chat + }: { + connection: SelectConnection + sampling: SelectSamplingConfig + contextConfig: SelectContextConfig + promptConfig: SelectPromptConfig + chat: SelectChat & { + chatCharacters?: SelectChatCharacter & + { character: SelectCharacter }[] + chatPersonas?: SelectChatPersona & { persona: SelectPersona }[] + chatMessages: SelectChatMessage[] + } + }) { + this.connection = connection + this.sampling = sampling + this.contextConfig = contextConfig + this.promptConfig = promptConfig + this.chat = chat + } - // --- Default Ollama connection config --- - static connectionDefaults = { - baseUrl: "http://localhost:11434/", - promptFormat: PromptFormats.VICUNA, - tokenCounter: TokenCounterOptions.ESTIMATE, - extraJson: { - stream: true, - think: false, - keepAlive: "300ms", - raw: true - } - } + // --- Default Ollama connection config --- + static connectionDefaults = { + baseUrl: "http://localhost:11434/", + promptFormat: PromptFormats.VICUNA, + tokenCounter: TokenCounterOptions.ESTIMATE, + extraJson: { + stream: true, + think: false, + keepAlive: "300ms", + raw: true + } + } - static defaultContextLimit = 2048 - static contextThresholdPercent = 0.9 // we don't want to hit the limit, so we stop at 90% of the context size + static defaultContextLimit = 2048 + static contextThresholdPercent = 0.9 // we don't want to hit the limit, so we stop at 90% of the context size - // --- Context builders --- - contextBuildCharacterDescription(character: any): string | undefined { - if (!character?.description) return undefined - return `**Assistant character {{char}}'s description:**\n\n${character.description}\n\n` - } - contextBuildCharacterPersonality(character: any): string | undefined { - if (!character?.personality) return undefined - return `**Assistant character {{char}}'s personality:**\n\n${character.personality}\n\n` - } - contextBuildCharacterScenario(character: any): string | undefined { - if (!character?.scenario) return undefined - return `**Assistant character {{char}}'s scenario:**\n\n${character.scenario}\n\n` - } - contextBuildCharacterWiBefore(): string | undefined { - return undefined - } - contextBuildCharacterWiAfter(): string | undefined { - return undefined - } - contextBuildPersonaDescription(persona: any): string | undefined { - if (!persona?.description) return undefined - return `**User character {{user}}'s description:**\n\n${persona.description}\n\n` - } - contextBuildSystemPrompt(): string | undefined { - if (!this.promptConfig.systemPrompt) return undefined - return `**Instructions:**\n\n${this.promptConfig.systemPrompt}\n\n` - } + // --- Context builders --- + contextBuildCharacterDescription(character: any): string | undefined { + if (!character?.description) return undefined + return `**Assistant character {{char}}'s description:**\n\n${character.description}\n\n` + } + contextBuildCharacterPersonality(character: any): string | undefined { + if (!character?.personality) return undefined + return `**Assistant character {{char}}'s personality:**\n\n${character.personality}\n\n` + } + contextBuildCharacterScenario(character: any): string | undefined { + if (!character?.scenario) return undefined + return `**Assistant character {{char}}'s scenario:**\n\n${character.scenario}\n\n` + } + contextBuildCharacterWiBefore(): string | undefined { + return undefined + } + contextBuildCharacterWiAfter(): string | undefined { + return undefined + } + contextBuildPersonaDescription(persona: any): string | undefined { + if (!persona?.description) return undefined + return `**User character {{user}}'s description:**\n\n${persona.description}\n\n` + } + contextBuildSystemPrompt(): string | undefined { + if (!this.promptConfig.systemPrompt) return undefined + return `**Instructions:**\n\n${this.promptConfig.systemPrompt}\n\n` + } - // --- SamplingConfig mapping --- - static ollamaKeyMap: Record = { - temperature: "temperature", - topP: "top_p", - topK: "top_k", - repetitionPenalty: "repetition_penalty", - minP: "min_p", - tfs: "tfs", - typicalP: "typical_p", - mirostat: "mirostat", - mirostatTau: "mirostat_tau", - mirostatEta: "mirostat_eta", - penaltyAlpha: "penalty_alpha", - frequencyPenalty: "frequency_penalty", - presencePenalty: "presence_penalty", - responseTokens: "num_predict", - contextTokens: "num_ctx", - noRepeatNgramSize: "no_repeat_ngram_size", - numBeams: "num_beams", - lengthPenalty: "length_penalty", - minLength: "min_length", - encoderRepetitionPenalty: "encoder_repetition_penalty", - freqPen: "freq_pen", - presencePen: "presence_pen", - skew: "skew", - doSample: "do_sample", - earlyStopping: "early_stopping", - dynatemp: "dynatemp", - minTemp: "min_temp", - maxTemp: "max_temp", - dynatempExponent: "dynatemp_exponent", - smoothingFactor: "smoothing_factor", - smoothingCurve: "smoothing_curve", - dryAllowedLength: "dry_allowed_length", - dryMultiplier: "dry_multiplier", - dryBase: "dry_base", - dryPenaltyLastN: "dry_penalty_last_n", - maxTokensSecond: "max_tokens_second", - seed: "seed", - addBosToken: "add_bos_token", - banEosToken: "ban_eos_token", - skipSpecialTokens: "skip_special_tokens", - includeReasoning: "include_reasoning", - streaming: "streaming", // Not sent to Ollama, handled separately - mirostatMode: "mirostat_mode", - xtcThreshold: "xtc_threshold", - xtcProbability: "xtc_probability", - nsigma: "nsigma", - speculativeNgram: "speculative_ngram", - guidanceScale: "guidance_scale", - etaCutoff: "eta_cutoff", - epsilonCutoff: "epsilon_cutoff", - repPenRange: "rep_pen_range", - repPenDecay: "rep_pen_decay", - repPenSlope: "rep_pen_slope", - logitBias: "logit_bias", - bannedTokens: "banned_tokens" - } + // --- SamplingConfig mapping --- + static ollamaKeyMap: Record = { + temperature: "temperature", + topP: "top_p", + topK: "top_k", + repetitionPenalty: "repetition_penalty", + minP: "min_p", + tfs: "tfs", + typicalP: "typical_p", + mirostat: "mirostat", + mirostatTau: "mirostat_tau", + mirostatEta: "mirostat_eta", + penaltyAlpha: "penalty_alpha", + frequencyPenalty: "frequency_penalty", + presencePenalty: "presence_penalty", + responseTokens: "num_predict", + contextTokens: "num_ctx", + noRepeatNgramSize: "no_repeat_ngram_size", + numBeams: "num_beams", + lengthPenalty: "length_penalty", + minLength: "min_length", + encoderRepetitionPenalty: "encoder_repetition_penalty", + freqPen: "freq_pen", + presencePen: "presence_pen", + skew: "skew", + doSample: "do_sample", + earlyStopping: "early_stopping", + dynatemp: "dynatemp", + minTemp: "min_temp", + maxTemp: "max_temp", + dynatempExponent: "dynatemp_exponent", + smoothingFactor: "smoothing_factor", + smoothingCurve: "smoothing_curve", + dryAllowedLength: "dry_allowed_length", + dryMultiplier: "dry_multiplier", + dryBase: "dry_base", + dryPenaltyLastN: "dry_penalty_last_n", + maxTokensSecond: "max_tokens_second", + seed: "seed", + addBosToken: "add_bos_token", + banEosToken: "ban_eos_token", + skipSpecialTokens: "skip_special_tokens", + includeReasoning: "include_reasoning", + streaming: "streaming", // Not sent to Ollama, handled separately + mirostatMode: "mirostat_mode", + xtcThreshold: "xtc_threshold", + xtcProbability: "xtc_probability", + nsigma: "nsigma", + speculativeNgram: "speculative_ngram", + guidanceScale: "guidance_scale", + etaCutoff: "eta_cutoff", + epsilonCutoff: "epsilon_cutoff", + repPenRange: "rep_pen_range", + repPenDecay: "rep_pen_decay", + repPenSlope: "rep_pen_slope", + logitBias: "logit_bias", + bannedTokens: "banned_tokens" + } - mapSamplingConfig(): Record { - const result: Record = {} - for (const [key, value] of Object.entries(this.sampling)) { - if (key.endsWith("Enabled")) continue - const enabledKey = key + "Enabled" - if ((this.sampling as any)[enabledKey] === false) continue - if ((this.constructor as typeof OllamaAdapter).ollamaKeyMap[key]) { - if (key === "streaming") continue - result[(this.constructor as typeof OllamaAdapter).ollamaKeyMap[key]] = value - } - } - return result - } + mapSamplingConfig(): Record { + const result: Record = {} + for (const [key, value] of Object.entries(this.sampling)) { + if (key.endsWith("Enabled")) continue + const enabledKey = key + "Enabled" + if ((this.sampling as any)[enabledKey] === false) continue + if ((this.constructor as typeof OllamaAdapter).ollamaKeyMap[key]) { + if (key === "streaming") continue + result[ + (this.constructor as typeof OllamaAdapter).ollamaKeyMap[key] + ] = value + } + } + return result + } - // --- API helpers --- - static async testConnection(connection: SelectConnection): Promise<{ ok: boolean; error?: string }> { - try { - const ollama = new Ollama({ - host: connection.baseUrl - }) - const res = await ollama.list() - if (res && Array.isArray(res.models)) { - return { ok: true } - } else { - console.log("Ollama testConnection response:", res) - return { ok: false, error: "Unexpected response format from Ollama API" } - } - } catch (e: any) { - console.error("Ollama testConnection error:", e) - return { ok: false, error: e.message || String(e) } - } - } + // --- API helpers --- + static async testConnection( + connection: SelectConnection + ): Promise<{ ok: boolean; error?: string }> { + try { + const ollama = new Ollama({ + host: connection.baseUrl + }) + const res = await ollama.list() + if (res && Array.isArray(res.models)) { + return { ok: true } + } else { + console.log("Ollama testConnection response:", res) + return { + ok: false, + error: "Unexpected response format from Ollama API" + } + } + } catch (e: any) { + console.error("Ollama testConnection error:", e) + return { ok: false, error: e.message || String(e) } + } + } - static async listModels(connection: SelectConnection): Promise<{ models: any[]; error?: string }> { - try { - const ollama = new Ollama({ - host: connection.baseUrl - }) - const res = await ollama.list() - if (res && Array.isArray(res.models)) { - return { models: res.models } - } else { - console.log("Ollama listModels response:", res) - return { models: [], error: "Unexpected response format from Ollama API" } - } - } catch (e: any) { - console.error("Ollama listModels error:", e) - return { models: [], error: e.message || String(e) } - } - } + static async listModels( + connection: SelectConnection + ): Promise<{ models: any[]; error?: string }> { + try { + const ollama = new Ollama({ + host: connection.baseUrl + }) + const res = await ollama.list() + if (res && Array.isArray(res.models)) { + return { models: res.models } + } else { + console.log("Ollama listModels response:", res) + return { + models: [], + error: "Unexpected response format from Ollama API" + } + } + } catch (e: any) { + console.error("Ollama listModels error:", e) + return { models: [], error: e.message || String(e) } + } + } - // --- Prompt construction --- - async compilePrompt(): Promise<[string, number, number, number, number]> { - const characterName = this.chat.chatCharacters?.[0]?.character?.name || "assistant" - const persona = this.chat.chatPersonas?.[0]?.persona - const personaName = persona?.name || "user" - const character = this.chat.chatCharacters?.[0]?.character - const systemCtxData: Record = { - char: characterName, - character: characterName, - user: personaName, - persona: this.contextBuildPersonaDescription(persona), - personaDescription: this.contextBuildPersonaDescription(persona), - description: this.contextBuildCharacterDescription(character), - personality: this.contextBuildCharacterPersonality(character), - scenario: this.contextBuildCharacterScenario(character), - wiBefore: this.contextBuildCharacterWiBefore(), - wiAfter: this.contextBuildCharacterWiAfter(), - system: this.contextBuildSystemPrompt() - } - const systemTemplate = this.contextConfig.template || "{{system}}" - const renderedSystemBlock = Handlebars.compile(systemTemplate)(systemCtxData) - const systemBlock = PromptBlockFormatter.makeBlock({ - format: this.connection.promptFormat || "chatml", - role: "system", - content: renderedSystemBlock - }) + // --- Prompt construction --- + async compilePrompt(): Promise<[string, number, number, number, number]> { + const characterName = + this.chat.chatCharacters?.[0]?.character?.nickname || + this.chat.chatCharacters?.[0]?.character?.name || + "assistant" + const persona = this.chat.chatPersonas?.[0]?.persona + const personaName = persona?.name || "user" + const character = this.chat.chatCharacters?.[0]?.character + const systemCtxData: Record = { + char: characterName, + character: characterName, + user: personaName, + persona: this.contextBuildPersonaDescription(persona), + personaDescription: this.contextBuildPersonaDescription(persona), + description: this.contextBuildCharacterDescription(character), + personality: this.contextBuildCharacterPersonality(character), + scenario: this.contextBuildCharacterScenario(character), + wiBefore: this.contextBuildCharacterWiBefore(), + wiAfter: this.contextBuildCharacterWiAfter(), + system: this.contextBuildSystemPrompt() + } + const systemTemplate = this.contextConfig.template || "{{system}}" + const renderedSystemBlock = + Handlebars.compile(systemTemplate)(systemCtxData) + const systemBlock = PromptBlockFormatter.makeBlock({ + format: this.connection.promptFormat || "chatml", + role: "system", + content: renderedSystemBlock + }) - // --- Context window logic --- - const messages = this.chat.chatMessages.filter((msg: SelectChatMessage) => !msg.isHidden) - const totalMessages = messages.length - const reversed = [...messages].reverse() - const promptBlocks: string[] = [systemBlock] - const messageBlocks: string[] = [] - let tokenCounter: any - let tokenLimit: number - let contextThreshold: number - let totalTokens = 0 - let alwaysInclude = 2 // Always include the 2 most recent messages - tokenCounter = this.getTokenCounter() - if (this.sampling.contextTokensEnabled && typeof this.sampling.contextTokens === 'number') { - tokenLimit = this.sampling.contextTokens - } else { - tokenLimit = (this.constructor as typeof OllamaAdapter).defaultContextLimit - } - contextThreshold = Math.floor(tokenLimit * (this.constructor as typeof OllamaAdapter).contextThresholdPercent) - let includedMessages = 0 - for (let i = 0; i < reversed.length; i++) { - const msg = reversed[i] - const block = PromptBlockFormatter.makeBlock({ - format: this.connection.promptFormat || "chatml", - role: msg.role! || "assistant", - content: `[{{${msg.role === "user" ? "user" : msg.role === "assistant" ? "char" : msg.role === "system" ? "system" : "system"}}}]:\n${msg.content}` - }) - messageBlocks.unshift(block) - if (i < alwaysInclude) { - includedMessages++ - continue - } - const currentPrompt = [systemBlock, ...messageBlocks].join("\n\n") - const tokens = tokenCounter.countTokens(currentPrompt) - if (tokens > tokenLimit) { - messageBlocks.shift() - break - } - if (tokens > contextThreshold) { - includedMessages++ - break - } - includedMessages++ - } - promptBlocks.push(...messageBlocks) - if (this.contextConfig.alwaysForceName) { - promptBlocks.push( - PromptBlockFormatter.makeBlock({ - format: this.connection.promptFormat || "chatml", - role: "assistant", - content: "{{char}}:", - includeClose: false - }) - ) - } - const prompt = Handlebars.compile(promptBlocks.join("\n\n"))(systemCtxData) - totalTokens = tokenCounter.countTokens(prompt) - return [prompt.trim(), totalTokens, tokenLimit, includedMessages, totalMessages] - } + // --- Context window logic --- + const messages = this.chat.chatMessages.filter( + (msg: SelectChatMessage) => !msg.isHidden + ) + const totalMessages = messages.length + const reversed = [...messages].reverse() + const promptBlocks: string[] = [systemBlock] + const messageBlocks: string[] = [] + let tokenCounter: any + let tokenLimit: number + let contextThreshold: number + let totalTokens = 0 + let alwaysInclude = 2 // Always include the 2 most recent messages + tokenCounter = this.getTokenCounter() + if ( + this.sampling.contextTokensEnabled && + typeof this.sampling.contextTokens === "number" + ) { + tokenLimit = this.sampling.contextTokens + } else { + tokenLimit = (this.constructor as typeof OllamaAdapter) + .defaultContextLimit + } + contextThreshold = Math.floor( + tokenLimit * + (this.constructor as typeof OllamaAdapter) + .contextThresholdPercent + ) + let includedMessages = 0 + for (let i = 0; i < reversed.length; i++) { + const msg = reversed[i] + const block = PromptBlockFormatter.makeBlock({ + format: this.connection.promptFormat || "chatml", + role: msg.role! || "assistant", + content: `[{{${msg.role === "user" ? "user" : msg.role === "assistant" ? "char" : msg.role === "system" ? "system" : "system"}}}]:\n${msg.content}` + }) + messageBlocks.unshift(block) + if (i < alwaysInclude) { + includedMessages++ + continue + } + const currentPrompt = [systemBlock, ...messageBlocks].join("\n\n") + const tokens = tokenCounter.countTokens(currentPrompt) + if (tokens > tokenLimit) { + messageBlocks.shift() + break + } + if (tokens > contextThreshold) { + includedMessages++ + break + } + includedMessages++ + } + promptBlocks.push(...messageBlocks) + if (this.contextConfig.alwaysForceName) { + promptBlocks.push( + PromptBlockFormatter.makeBlock({ + format: this.connection.promptFormat || "chatml", + role: "assistant", + content: "{{char}}:", + includeClose: false + }) + ) + } + const prompt = Handlebars.compile(promptBlocks.join("\n\n"))( + systemCtxData + ) + totalTokens = tokenCounter.countTokens(prompt) + return [ + prompt.trim(), + totalTokens, + tokenLimit, + includedMessages, + totalMessages + ] + } - // --- Ollama client instance --- - getClient() { - if (!this._client) { - const host = this.connection.baseUrl || OllamaAdapter.connectionDefaults.baseUrl; - this._client = new Ollama({ host }); - } - return this._client; - } + // --- Ollama client instance --- + getClient() { + if (!this._client) { + const host = + this.connection.baseUrl || + OllamaAdapter.connectionDefaults.baseUrl + this._client = new Ollama({ host }) + } + return this._client + } - getTokenCounter() { - if (!this._tokenCounter) { - this._tokenCounter = new TokenCounters(this.connection.tokenCounter || TokenCounterOptions.ESTIMATE); - } - return this._tokenCounter; - } + getTokenCounter() { + if (!this._tokenCounter) { + this._tokenCounter = new TokenCounters( + this.connection.tokenCounter || TokenCounterOptions.ESTIMATE + ) + } + return this._tokenCounter + } - static mapRole(role: string): string { - if (role === "system") return "system" - if (role === "assistant" || role === "bot") return "assistant" - return "user" - } + static mapRole(role: string): string { + if (role === "system") return "system" + if (role === "assistant" || role === "bot") return "assistant" + return "user" + } - async generate(): Promise void) => Promise)> { - const model = this.connection.model ?? OllamaAdapter.connectionDefaults.baseUrl - const stream = this.connection!.extraJson?.stream || false - const think = this.connection!.extraJson?.think || false - const keep_alive = this.connection!.extraJson?.keepAlive || "300ms" - const raw = this.connection!.extraJson?.raw || false - if (typeof model !== "string") throw new Error("OllamaAdapter: model must be a string") + async generate(): Promise< + string | ((cb: (chunk: string) => void) => Promise) + > { + const model = + this.connection.model ?? OllamaAdapter.connectionDefaults.baseUrl + const stream = this.connection!.extraJson?.stream || false + const think = this.connection!.extraJson?.think || false + const keep_alive = this.connection!.extraJson?.keepAlive || "300ms" + const raw = this.connection!.extraJson?.raw || false + if (typeof model !== "string") + throw new Error("OllamaAdapter: model must be a string") - // Prepare stop strings for Ollama - const stopStrings = StopStrings.get(this.connection.promptFormat || "chatml") - const characterName = this.chat.chatCharacters?.[0]?.character?.name || "assistant" - const personaName = this.chat.chatPersonas?.[0]?.persona?.name || "user" - const stopContext: Record = { char: characterName, user: personaName } - const stop = stopStrings.map(str => Handlebars.compile(str)(stopContext)) + // Prepare stop strings for Ollama + const stopStrings = StopStrings.get( + this.connection.promptFormat || "chatml" + ) + const characterName = + this.chat.chatCharacters?.[0]?.character?.nickname || + this.chat.chatCharacters?.[0]?.character?.nickname || + this.chat.chatCharacters?.[0]?.character?.name || + "assistant" + const personaName = this.chat.chatPersonas?.[0]?.persona?.name || "user" + const stopContext: Record = { + char: characterName, + user: personaName + } + const stop = stopStrings.map((str) => + Handlebars.compile(str)(stopContext) + ) - // Await the prompt and token count - const [prompt, totalTokens, tokenLimit] = await this.compilePrompt() + // Await the prompt and token count + const [prompt, totalTokens, tokenLimit] = await this.compilePrompt() - const req = { - model, - prompt, - stream, - think, - raw, - keep_alive, - options: { - ...this.mapSamplingConfig(), - stop - } - } - if (stream) { - return async (cb: (chunk: string) => void) => { - let content = "" - try { - const ollama = this.getClient() - const result = await ollama.generate({ ...req, stream: true }) - for await (const part of result) { - if (part.response) { - content += part.response - cb(part.response) - } - } - // No need to apply stop strings here, Ollama will handle it - } catch (e: any) { - cb("FAILURE: " + (e.message || String(e))) - } - } - } else { - return (async () => { - let content = "" - try { - const ollama = this.getClient() - const result = await ollama.generate({ ...req, stream: false }) - if (result && typeof result === "object" && "response" in result) { - content = result.response || "" - // No need to apply stop strings here, Ollama will handle it - return content - } else { - return "FAILURE: Unexpected Ollama result type" - } - } catch (e: any) { - return "FAILURE: " + (e.message || String(e)) - } - })() - } - } + const req = { + model, + prompt, + stream, + think, + raw, + keep_alive, + options: { + ...this.mapSamplingConfig(), + stop + } + } + if (stream) { + return async (cb: (chunk: string) => void) => { + let content = "" + try { + const ollama = this.getClient() + const result = await ollama.generate({ + ...req, + stream: true + }) + for await (const part of result) { + if (part.response) { + content += part.response + cb(part.response) + } + } + // No need to apply stop strings here, Ollama will handle it + } catch (e: any) { + cb("FAILURE: " + (e.message || String(e))) + } + } + } else { + return (async () => { + let content = "" + try { + const ollama = this.getClient() + const result = await ollama.generate({ + ...req, + stream: false + }) + if ( + result && + typeof result === "object" && + "response" in result + ) { + content = result.response || "" + // No need to apply stop strings here, Ollama will handle it + return content + } else { + return "FAILURE: Unexpected Ollama result type" + } + } catch (e: any) { + return "FAILURE: " + (e.message || String(e)) + } + })() + } + } - // --- Abort in-flight Ollama request --- - abort() { - const client = this.getClient(); - if (typeof client.abort === 'function') { - client.abort(); - } - } + // --- Abort in-flight Ollama request --- + abort() { + const client = this.getClient() + if (typeof client.abort === "function") { + client.abort() + } + } } diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 83d3fb3..0122502 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,15 +1,31 @@ -import { updated } from '$app/state'; -import { relations } from 'drizzle-orm'; -import { sqliteTable, integer, text, numeric, real, blob, SQLiteBoolean } from 'drizzle-orm/sqlite-core'; -import { TokenCounterManager } from '../utils/TokenCounterManager'; +import { updated } from "$app/state" +import { relations, sql } from "drizzle-orm" +import { + sqliteTable, + integer, + text, + numeric, + real, + blob, + SQLiteBoolean +} from "drizzle-orm/sqlite-core" +import { TokenCounterManager } from "../utils/TokenCounterManager" -export const users = sqliteTable('users', { - id: integer('id').primaryKey(), - username: text('username').notNull(), - activeConnectionId: integer('active_connection_id').references(() => connections.id, {onDelete: 'set null'}), - activeSamplingConfigId: integer('active_sampling_id').references(() => samplingConfigs.id, {onDelete: 'set null'}), - activeContextConfigId: integer('active_context_config_id').references(() => contextConfigs.id, {onDelete: 'set null'}), - activePromptConfigId: integer('active_prompt_config_id').references(() => promptConfigs.id, {onDelete: 'set null'}), +export const users = sqliteTable("users", { + id: integer("id").primaryKey(), + username: text("username").notNull(), + activeConnectionId: integer("active_connection_id").references(() => connections.id, { + onDelete: "set null" + }), + activeSamplingConfigId: integer("active_sampling_id").references(() => samplingConfigs.id, { + onDelete: "set null" + }), + activeContextConfigId: integer("active_context_config_id").references(() => contextConfigs.id, { + onDelete: "set null" + }), + activePromptConfigId: integer("active_prompt_config_id").references(() => promptConfigs.id, { + onDelete: "set null" + }) }) export const userRelations = relations(users, ({ many, one }) => ({ @@ -31,91 +47,97 @@ export const userRelations = relations(users, ({ many, one }) => ({ fields: [users.activePromptConfigId], references: [promptConfigs.id] }), - personas: many(personas), + personas: many(personas) })) -export const samplingConfigs = sqliteTable('sampling_configs', { - id: integer('id').primaryKey(), - name: text('name').notNull(), // Name for this sampling config (for selection) - isImmutable: integer('is_immutable', {mode: 'boolean'}).default(0), // Is this the built-in config? Then we don't want to allow mutation/deletion +export const samplingConfigs = sqliteTable("sampling_configs", { + id: integer("id").primaryKey(), + name: text("name").notNull(), // Name for this sampling config (for selection) + isImmutable: integer("is_immutable", { mode: "boolean" }).default(0), // Is this the built-in config? Then we don't want to allow mutation/deletion // Tuned defaults for roleplay: // More creative and less repetitive - temperature: real('temperature').default(0.7), // Higher = more creative - temperatureEnabled: integer('temperature_enabled', {mode: 'boolean'}).default(true), + temperature: real("temperature").default(0.7), // Higher = more creative + temperatureEnabled: integer("temperature_enabled", { mode: "boolean" }).default(true), - topP: real('top_p').default(0.92), // Lower than 1, encourages diversity but not too random - topPEnabled: integer('top_p_enabled', {mode: 'boolean'}).default(true), + topP: real("top_p").default(0.92), // Lower than 1, encourages diversity but not too random + topPEnabled: integer("top_p_enabled", { mode: "boolean" }).default(true), - topK: integer('top_k').default(80), // Allows more token options for creative replies - topKEnabled: integer('top_k_enabled', {mode: 'boolean'}).default(true), + topK: integer("top_k").default(80), // Allows more token options for creative replies + topKEnabled: integer("top_k_enabled", { mode: "boolean" }).default(true), - repetitionPenalty: real('repetition_penalty').default(1.15), // Slightly encourages less repetition but not too harsh - repetitionPenaltyEnabled: integer('repetition_penalty_enabled', {mode: 'boolean'}).default(true), + repetitionPenalty: real("repetition_penalty").default(1.15), // Slightly encourages less repetition but not too harsh + repetitionPenaltyEnabled: integer("repetition_penalty_enabled", { mode: "boolean" }).default( + true + ), - frequencyPenalty: real('frequency_penalty').default(0.2), // Mild penalty for repetitive phrases - frequencyPenaltyEnabled: integer('frequency_penalty_enabled', {mode: 'boolean'}).default(true), + frequencyPenalty: real("frequency_penalty").default(0.2), // Mild penalty for repetitive phrases + frequencyPenaltyEnabled: integer("frequency_penalty_enabled", { mode: "boolean" }).default( + true + ), - presencePenalty: real('presence_penalty').default(0.6), // Encourage new topics and freshness - presencePenaltyEnabled: integer('presence_penalty_enabled', {mode: 'boolean'}).default(true), + presencePenalty: real("presence_penalty").default(0.6), // Encourage new topics and freshness + presencePenaltyEnabled: integer("presence_penalty_enabled", { mode: "boolean" }).default(true), - responseTokens: integer('response_tokens').default(512), // Allow longer, richer replies - responseTokensEnabled: integer('response_tokens_enabled', {mode: 'boolean'}).default(true), - responseTokensUnlocked: integer('response_tokens_unlocked', {mode: 'boolean'}).default(false), // Dynamic length allowed + responseTokens: integer("response_tokens").default(512), // Allow longer, richer replies + responseTokensEnabled: integer("response_tokens_enabled", { mode: "boolean" }).default(true), + responseTokensUnlocked: integer("response_tokens_unlocked", { mode: "boolean" }).default(false), // Dynamic length allowed - contextTokens: integer('context_tokens').default(4096), // Keep more conversation in memory/context - contextTokensEnabled: integer('context_tokens_enabled', {mode: 'boolean'}).default(true), - contextTokensUnlocked: integer('context_tokens_unlocked', {mode: 'boolean'}).default(false), // Allow for context window expansion + contextTokens: integer("context_tokens").default(4096), // Keep more conversation in memory/context + contextTokensEnabled: integer("context_tokens_enabled", { mode: "boolean" }).default(true), + contextTokensUnlocked: integer("context_tokens_unlocked", { mode: "boolean" }).default(false), // Allow for context window expansion - seed: integer('seed').default(-1), // -1 for random, can be used for deterministic sampling - seedEnabled: integer('seed_enabled', {mode: 'boolean'}).default(false), + seed: integer("seed").default(-1), // -1 for random, can be used for deterministic sampling + seedEnabled: integer("seed_enabled", { mode: "boolean" }).default(false) }) export const samplingRelations = relations(samplingConfigs, () => ({})) -export const connections = sqliteTable('connections', { - id: integer('id').primaryKey(), - name: text('name').notNull(), // Connection name (e.g., ollama, llama, chatgpt) - type: text('type').notNull(), // Connection type/category (e.g., ollama, chatgpt, etc) - baseUrl: text('base_url'), // Base URL or endpoint for API - model: text('model'), // Model name or identifier +export const connections = sqliteTable("connections", { + id: integer("id").primaryKey(), + name: text("name").notNull(), // Connection name (e.g., ollama, llama, chatgpt) + type: text("type").notNull(), // Connection type/category (e.g., ollama, chatgpt, etc) + baseUrl: text("base_url"), // Base URL or endpoint for API + model: text("model"), // Model name or identifier // Ollama-specific options - extraJson: text('extra_json', { mode: 'json' }).$type>(), // Additional JSON options for the connections, api keys, etc. - tokenCounter: text('token_counter').notNull().default("estimate"), - promptFormat: text('prompt_format').default('vicuna') + extraJson: text("extra_json", { mode: "json" }).$type>(), // Additional JSON options for the connections, api keys, etc. + tokenCounter: text("token_counter").notNull().default("estimate"), + promptFormat: text("prompt_format").default("vicuna") }) export const connectionsRelations = relations(connections, () => ({})) -export const contextConfigs = sqliteTable('context_configs', { - id: integer('id').primaryKey(), - isImmutable: integer('is_immutable', {mode: "boolean"}).default(true), - name: text('name').notNull(), - template: text('template'), // Sillytavern storyString - alwaysForceName: integer('always_force_name', {mode: 'boolean'}).default(true), // Always force name2 +export const contextConfigs = sqliteTable("context_configs", { + id: integer("id").primaryKey(), + isImmutable: integer("is_immutable", { mode: "boolean" }).default(true), + name: text("name").notNull(), + template: text("template"), // Sillytavern storyString + alwaysForceName: integer("always_force_name", { mode: "boolean" }).default(true) // Always force name2 }) export const contextConfigsRelations = relations(contextConfigs, () => ({})) -export const promptConfigs = sqliteTable('prompt_configs', { - id: integer('id').primaryKey(), - isImmutable: integer('is_immutable', {mode: "boolean"}).default(true), - name: text('name').notNull(), - systemPrompt: text('system_prompt'), // Maps to sillytavern sysPrompt.content +export const promptConfigs = sqliteTable("prompt_configs", { + id: integer("id").primaryKey(), + isImmutable: integer("is_immutable", { mode: "boolean" }).default(true), + name: text("name").notNull(), + systemPrompt: text("system_prompt") // Maps to sillytavern sysPrompt.content }) export const promptConfigsRelations = relations(promptConfigs, () => ({})) -export const lorebooks = sqliteTable('lorebooks', { - id: integer('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, {onDelete: 'cascade'}), // FK to users.id - name: text('name').notNull(), // Lorebook name - description: text('description'), // Lorebook description - tags: text('tags'), // JSON array of tags - entries: text('entries'), // JSON array of lorebook entries (for compatibility with SillyTavern) - metadata: text('metadata'), // JSON object for any extra SillyTavern/world/lorebook fields - createdAt: text('created_at'), // ISO date string - updatedAt: text('updated_at'), // ISO date string +export const lorebooks = sqliteTable("lorebooks", { + id: integer("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), // FK to users.id + name: text("name").notNull(), // Lorebook name + description: text("description"), // Lorebook description + tags: text("tags"), // JSON array of tags + entries: text("entries"), // JSON array of lorebook entries (for compatibility with SillyTavern) + metadata: text("metadata"), // JSON object for any extra SillyTavern/world/lorebook fields + createdAt: text("created_at"), // ISO date string + updatedAt: text("updated_at") // ISO date string }) export const lorebooksRelations = relations(lorebooks, ({ many, one }) => ({ @@ -126,40 +148,42 @@ export const lorebooksRelations = relations(lorebooks, ({ many, one }) => ({ }) })) -export const lorebookEntries = sqliteTable('lorebook_entries', { - id: integer('id').primaryKey(), - lorebookId: integer('lorebook_id').notNull().references(() => lorebooks.id, {onDelete: 'cascade'}), // FK to lorebooks.id - key: text('key'), // JSON array of keys - keySecondary: text('key_secondary'), // JSON array of secondary keys - comment: text('comment'), - content: text('content'), - constant: integer('constant', {mode: "boolean"}).default(false), // Is this entry a constant value? - vectorized: integer('vectorized'), - selective: integer('selective'), - selectiveLogic: integer('selective_logic'), - addMemo: integer('add_memo'), - order: integer('order'), - position: integer('position'), - disable: integer('disable', {mode: "boolean"}).default(false), // Is this entry disabled? - excludeRecursion: integer('exclude_recursion'), - preventRecursion: integer('prevent_recursion'), - delayUntilRecursion: integer('delay_until_recursion'), - probability: integer('probability'), - useProbability: integer('use_probability'), - depth: integer('depth'), - group: text('group'), - groupOverride: integer('group_override'), - groupWeight: integer('group_weight'), - scanDepth: integer('scan_depth'), - caseSensitive: integer('case_sensitive'), - matchWholeWords: integer('match_whole_words'), - useGroupScoring: integer('use_group_scoring'), - automationId: text('automation_id'), - role: text('role'), - sticky: integer('sticky'), - cooldown: integer('cooldown'), - delay: integer('delay'), - displayIndex: integer('display_index'), +export const lorebookEntries = sqliteTable("lorebook_entries", { + id: integer("id").primaryKey(), + lorebookId: integer("lorebook_id") + .notNull() + .references(() => lorebooks.id, { onDelete: "cascade" }), // FK to lorebooks.id + key: text("key"), // JSON array of keys + keySecondary: text("key_secondary"), // JSON array of secondary keys + comment: text("comment"), + content: text("content"), + constant: integer("constant", { mode: "boolean" }).default(false), // Is this entry a constant value? + vectorized: integer("vectorized"), + selective: integer("selective"), + selectiveLogic: integer("selective_logic"), + addMemo: integer("add_memo"), + order: integer("order"), + position: integer("position"), + disable: integer("disable", { mode: "boolean" }).default(false), // Is this entry disabled? + excludeRecursion: integer("exclude_recursion"), + preventRecursion: integer("prevent_recursion"), + delayUntilRecursion: integer("delay_until_recursion"), + probability: integer("probability"), + useProbability: integer("use_probability"), + depth: integer("depth"), + group: text("group"), + groupOverride: integer("group_override"), + groupWeight: integer("group_weight"), + scanDepth: integer("scan_depth"), + caseSensitive: integer("case_sensitive"), + matchWholeWords: integer("match_whole_words"), + useGroupScoring: integer("use_group_scoring"), + automationId: text("automation_id"), + role: text("role"), + sticky: integer("sticky"), + cooldown: integer("cooldown"), + delay: integer("delay"), + displayIndex: integer("display_index") }) export const lorebookEntriesRelations = relations(lorebookEntries, ({ one }) => ({ @@ -169,19 +193,23 @@ export const lorebookEntriesRelations = relations(lorebookEntries, ({ one }) => }) })) -export const tags = sqliteTable('tags', { - id: integer('id').primaryKey(), - name: text('name').notNull(), // Tag name (unique) - description: text('description'), +export const tags = sqliteTable("tags", { + id: integer("id").primaryKey(), + name: text("name").notNull(), // Tag name (unique) + description: text("description") }) export const tagsRelations = relations(tags, ({ many }) => ({ characterTags: many(characterTags) })) -export const characterTags = sqliteTable('character_tags', { - characterId: integer('character_id').notNull().references(() => characters.id, {onDelete: 'cascade'}), // FK to characters.id - tagId: integer('tag_id').notNull().references(() => tags.id, {onDelete: 'cascade'}), // FK to tags.id +export const characterTags = sqliteTable("character_tags", { + characterId: integer("character_id") + .notNull() + .references(() => characters.id, { onDelete: "cascade" }), // FK to characters.id + tagId: integer("tag_id") + .notNull() + .references(() => tags.id, { onDelete: "cascade" }) // FK to tags.id }) export const characterTagsRelations = relations(characterTags, ({ one }) => ({ @@ -195,21 +223,46 @@ export const characterTagsRelations = relations(characterTags, ({ one }) => ({ }) })) -export const characters = sqliteTable('characters', { - id: integer('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, {onDelete: 'cascade'}), // FK to users.id - name: text('name').notNull(), - description: text('description'), - personality: text('personality'), // Persona field - scenario: text('scenario'), - firstMessage: text('first_message'), - exampleDialogues: text('example_dialogues'), // JSON/text - metadata: text('metadata'), // JSON/text for extra fields - avatar: text('avatar'), // Path or URL to avatar image - createdAt: text('created_at'), - updatedAt: text('updated_at'), - lorebookId: integer('lorebook_id').references(() => lorebooks.id, {onDelete: 'set null'}), // Optional FK to lorebooks.id - isFavorite: integer('is_favorite', {mode: "boolean"}).default(false), // 1 if favorite, 0 otherwise +export const characters = sqliteTable("characters", { + id: integer("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), // FK to users.id + name: text("name").notNull(), + nickname: text("nickname"), // Optional nickname + characterVersion: text("character_version").default("1.0"), // Version of the character schema + description: text("description"), + personality: text("personality"), // Persona field + scenario: text("scenario"), + firstMessage: text("first_message"), + alternateGreetings: text("alternate_greetings", { mode: "json" }) + .default("[]") + .$type(), // JSON array of alternate greetings + exampleDialogues: text("example_dialogues"), // JSON/text + metadata: text("metadata"), // JSON/text for extra fields + avatar: text("avatar"), // Path or URL to avatar image + creatorNotes: text("creator_notes"), // Notes from the character creator + creatorNotesMultilingual: text("creator_notes_multilingual", { mode: "json" }) + .default("{}") + .$type>(), // Multilingual creator notes as JSON object + groupOnlyGreetings: text("group_only_greetings", { mode: "json" }) + .default("[]") + .$type(), // JSON array of greetings for group chats + postHistoryInstructions: text("post_history_instructions"), // Instructions for post-history processing + source: text("source", { mode: "json" }).default("[]").$type(), // JSON array of sources (e.g., URLs, books) + assets: text("assets", { mode: "json" }).default("[]").$type< + Array<{ + type: string + uri: string + name: string + ext: string + }> + >(), // JSON array of asset paths or URLs + createdAt: text("created_at").default(sql`(CURRENT_TIMESTAMP)`), + updatedAt: text("updated_at").$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), + lorebookId: integer("lorebook_id").references(() => lorebooks.id, { onDelete: "set null" }), // Optional FK to lorebooks.id + extensions: text("extensions", { mode: "json" }).default("[]").$type>(), + isFavorite: integer("is_favorite", { mode: "boolean" }).default(false) // 1 if favorite, 0 otherwise }) export const charactersRelations = relations(characters, ({ many, one }) => ({ @@ -218,41 +271,45 @@ export const charactersRelations = relations(characters, ({ many, one }) => ({ fields: [characters.userId], references: [users.id] }), - lorebook: one(lorebooks, { - fields: [characters.lorebookId], - references: [lorebooks.id] - }), + lorebook: one(lorebooks, { + fields: [characters.lorebookId], + references: [lorebooks.id] + }) })) -export const personas = sqliteTable('personas', { - id: integer('id').primaryKey(), - userId: integer('user_id').notNull().references(() => users.id, {onDelete: 'cascade'}), // FK to users.id - isDefault: integer('is_default', {mode: "boolean"}).default(false), // Is this the default persona for the user? - avatar: text('avatar'), // e.g. 'user-default.png', '1747379438925-Ryvn.png' - name: text('name').notNull(), // e.g. 'Warren', 'Master Desir' - description: text('description'), // Persona description (long text) - position: integer('position').default(0), - connections: text('connections'), // JSON array of connection IDs or objects - createdAt: text('created_at'), - updatedAt: text('updated_at'), +export const personas = sqliteTable("personas", { + id: integer("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), // FK to users.id + isDefault: integer("is_default", { mode: "boolean" }).default(false), // Is this the default persona for the user? + avatar: text("avatar"), // e.g. 'user-default.png', '1747379438925-Ryvn.png' + name: text("name").notNull(), // e.g. 'Warren', 'Master Desir' + description: text("description"), // Persona description (long text) + position: integer("position").default(0), + connections: text("connections"), // JSON array of connection IDs or objects + createdAt: text("created_at"), + updatedAt: text("updated_at") }) export const personasRelations = relations(personas, ({ one, many }) => ({ user: one(users, { fields: [personas.userId], references: [users.id] - }), + }) })) // Chats (group or 1:1) -export const chats = sqliteTable('chats', { - id: integer('id').primaryKey(), - name: text('name'), // Optional chat/group name - isGroup: integer('is_group').default(0), // 1 for group chat, 0 for 1:1 - userId: integer('user_id').notNull().references(() => users.id, {onDelete: 'cascade'}), - createdAt: text('created_at'), - updatedAt: text('updated_at'), - metadata: text('metadata'), // JSON for extra settings +export const chats = sqliteTable("chats", { + id: integer("id").primaryKey(), + name: text("name"), // Optional chat/group name + isGroup: integer("is_group").default(0), // 1 for group chat, 0 for 1:1 + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + createdAt: text("created_at"), + updatedAt: text("updated_at"), + metadata: text("metadata") // JSON for extra settings }) export const chatsRelations = relations(chats, ({ one, many }) => ({ @@ -262,25 +319,29 @@ export const chatsRelations = relations(chats, ({ one, many }) => ({ }), chatMessages: many(chatMessages), chatPersonas: many(chatPersonas), - chatCharacters: many(chatCharacters), + chatCharacters: many(chatCharacters) })) // Chat messages -export const chatMessages = sqliteTable('chat_messages', { - id: integer('id').primaryKey(), - chatId: integer('chat_id').notNull().references(() => chats.id, {onDelete: 'cascade'}), - userId: integer('user_id').notNull().references(() => users.id, {onDelete: 'cascade'}), // nullable for system/character messages - characterId: integer('character_id').references(() => characters.id, {onDelete: 'set null'}), // nullable - personaId: integer('persona_id').references(() => personas.id, {onDelete: 'set null'}), // nullable - role: text('role'), // 'user', 'character', 'system', etc - content: text('content').notNull(), - createdAt: text('created_at'), - updatedAt: text('updated_at'), - isEdited: integer('is_edited').default(0), // 1 if edited, 0 otherwise - metadata: text('metadata'), // JSON for extra info - isGenerating: integer('is_generating', {mode: "boolean"}).default(false), // 1 if processing, 0 otherwise - adapterId: text('adapter_id'), // UUID for in-flight adapter instance, nullable - isHidden: integer('is_hidden', {mode: "boolean"}).default(false), // Whether this message is processed or not +export const chatMessages = sqliteTable("chat_messages", { + id: integer("id").primaryKey(), + chatId: integer("chat_id") + .notNull() + .references(() => chats.id, { onDelete: "cascade" }), + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), // nullable for system/character messages + characterId: integer("character_id").references(() => characters.id, { onDelete: "set null" }), // nullable + personaId: integer("persona_id").references(() => personas.id, { onDelete: "set null" }), // nullable + role: text("role"), // 'user', 'character', 'system', etc + content: text("content").notNull(), + createdAt: text("created_at"), + updatedAt: text("updated_at"), + isEdited: integer("is_edited").default(0), // 1 if edited, 0 otherwise + metadata: text("metadata"), // JSON for extra info + isGenerating: integer("is_generating", { mode: "boolean" }).default(false), // 1 if processing, 0 otherwise + adapterId: text("adapter_id"), // UUID for in-flight adapter instance, nullable + isHidden: integer("is_hidden", { mode: "boolean" }).default(false) // Whether this message is processed or not }) export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({ @@ -299,21 +360,25 @@ export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({ persona: one(personas, { fields: [chatMessages.personaId], references: [personas.id] - }), + }) })) export const GroupReplyStrategies = { - MANUAL: 'manual', // User manually selects persona for each reply - ORDERED: 'ordered', // Replies follow the order of personas in the chat - NATURAL: 'natural', // Replies are assigned based on natural conversation flow + MANUAL: "manual", // User manually selects persona for each reply + ORDERED: "ordered", // Replies follow the order of personas in the chat + NATURAL: "natural" // Replies are assigned based on natural conversation flow } // Many-to-many: chats <-> personas -export const chatPersonas = sqliteTable('chat_personas', { - chatId: integer('chat_id').notNull().references(() => chats.id, {onDelete: 'cascade'}), - personaId: integer('persona_id').notNull().references(() => personas.id, {onDelete: 'cascade'}), - position: integer('position').default(0), // Position in the chat - group_reply_strategy: text('group_reply_strategy').default(GroupReplyStrategies.ORDERED), // How to handle group replies +export const chatPersonas = sqliteTable("chat_personas", { + chatId: integer("chat_id") + .notNull() + .references(() => chats.id, { onDelete: "cascade" }), + personaId: integer("persona_id") + .notNull() + .references(() => personas.id, { onDelete: "cascade" }), + position: integer("position").default(0), // Position in the chat + group_reply_strategy: text("group_reply_strategy").default(GroupReplyStrategies.ORDERED) // How to handle group replies }) export const chatPersonasRelations = relations(chatPersonas, ({ one }) => ({ @@ -324,15 +389,19 @@ export const chatPersonasRelations = relations(chatPersonas, ({ one }) => ({ persona: one(personas, { fields: [chatPersonas.personaId], references: [personas.id] - }), + }) })) // Many-to-many: chats <-> characters -export const chatCharacters = sqliteTable('chat_characters', { - chatId: integer('chat_id').notNull().references(() => chats.id, {onDelete: 'cascade'}), - characterId: integer('character_id').notNull().references(() => characters.id, {onDelete: 'cascade'}), - position: integer('position').default(0), // Position in the chat - isActive: integer('is_active', {mode: "boolean"}).default(false), // 1 if active in chat, 0 if not +export const chatCharacters = sqliteTable("chat_characters", { + chatId: integer("chat_id") + .notNull() + .references(() => chats.id, { onDelete: "cascade" }), + characterId: integer("character_id") + .notNull() + .references(() => characters.id, { onDelete: "cascade" }), + position: integer("position").default(0), // Position in the chat + isActive: integer("is_active", { mode: "boolean" }).default(false) // 1 if active in chat, 0 if not }) export const chatCharactersRelations = relations(chatCharacters, ({ one }) => ({ @@ -343,7 +412,5 @@ export const chatCharactersRelations = relations(chatCharacters, ({ one }) => ({ character: one(characters, { fields: [chatCharacters.characterId], references: [characters.id] - }), + }) })) - - diff --git a/src/lib/server/sockets/characters.ts b/src/lib/server/sockets/characters.ts index f154071..2f92eb9 100644 --- a/src/lib/server/sockets/characters.ts +++ b/src/lib/server/sockets/characters.ts @@ -3,171 +3,224 @@ import { and, eq } from "drizzle-orm" import * as schema from "$lib/server/db/schema" import * as fsPromises from "fs/promises" import { getCharacterDataDir, handleCharacterAvatarUpload } from "../utils" -import extractChunks from 'png-chunks-extract'; -import {decode as decodeText} from 'png-chunk-text'; +import { CharacterCard } from "@lenml/char-card-reader" +import fs from "fs" +import { fileTypeFromBuffer } from "file-type" export async function charactersList( - socket: any, - message: Sockets.CharactersList.Call, - emitToUser: (event: string, data: any) => void + socket: any, + message: Sockets.CharactersList.Call, + emitToUser: (event: string, data: any) => void ) { - const charactersList = await db.query.characters.findMany({ - columns: { - id: true, - name: true, - avatar: true, - isFavorite: true - }, - where: (c, { eq }) => eq(c.userId, 1) // TODO: Replace with actual user id - }) - const res: Sockets.CharactersList.Response = { charactersList } - emitToUser("charactersList", res) + const charactersList = await db.query.characters.findMany({ + columns: { + id: true, + name: true, + nickname: true, + avatar: true, + isFavorite: true + }, + where: (c, { eq }) => eq(c.userId, 1) // TODO: Replace with actual user id + }) + const res: Sockets.CharactersList.Response = { charactersList } + emitToUser("charactersList", res) } export async function character( - socket: any, - message: Sockets.Character.Call, - emitToUser: (event: string, data: any) => void + socket: any, + message: Sockets.Character.Call, + emitToUser: (event: string, data: any) => void ) { - const character = await db.query.characters.findFirst({ - where: (c, { eq }) => eq(c.id, message.id) - }) - if (character) { - const res: Sockets.Character.Response = { character } - emitToUser("character", res) - } + const character = await db.query.characters.findFirst({ + where: (c, { eq }) => eq(c.id, message.id) + }) + if (character) { + const res: Sockets.Character.Response = { character } + emitToUser("character", res) + } } export async function createCharacter( - socket: any, - message: Sockets.CreateCharacter.Call, - emitToUser: (event: string, data: any) => void + socket: any, + message: Sockets.CreateCharacter.Call, + emitToUser: (event: string, data: any) => void ) { - try { - const data = message.character - delete data.avatar // Remove avatar from character data to avoid conflicts - const [character] = await db - .insert(schema.characters) - .values({ ...message.character, userId: 1 }) - .returning() + try { + const data = message.character + delete data.avatar // Remove avatar from character data to avoid conflicts + const [character] = await db + .insert(schema.characters) + .values({ ...message.character, userId: 1 }) + .returning() - if (message.avatarFile) { - await handleCharacterAvatarUpload({ - character, - avatarFile: message.avatarFile - }) - } + if (message.avatarFile) { + await handleCharacterAvatarUpload({ + character, + avatarFile: message.avatarFile + }) + } - await charactersList(socket, {}, emitToUser) + await charactersList(socket, {}, emitToUser) - const res: Sockets.CreateCharacter.Response = { character } - emitToUser("createCharacter", res) - } catch (e: any) { - console.error("Error creating character:", e) - emitToUser("error", { error: e.message || "Failed to create character." }) - return - } + const res: Sockets.CreateCharacter.Response = { character } + emitToUser("createCharacter", res) + } catch (e: any) { + console.error("Error creating character:", e) + emitToUser("error", { + error: e.message || "Failed to create character." + }) + return + } } export async function updateCharacter( - socket: any, - message: Sockets.UpdateCharacter.Call, - emitToUser: (event: string, data: any) => void + socket: any, + message: Sockets.UpdateCharacter.Call, + emitToUser: (event: string, data: any) => void ) { - const data = message.character - const id = data.id - const userId = 1 // Replace with actual userId + const data = message.character + const id = data.id + const userId = 1 // Replace with actual userId - // Remove userId and id if present and optional - if ('userId' in data) (data as any).userId = undefined - if ('id' in data) (data as any).id = undefined - delete data.avatar // Remove avatar from character data to avoid conflicts - const [updated] = await db - .update(schema.characters) - .set(data) - .where(and(eq(schema.characters.id, id), eq(schema.characters.userId, userId))) - .returning() + // Remove userId and id if present and optional + if ("userId" in data) (data as any).userId = undefined + if ("id" in data) (data as any).id = undefined + delete data.avatar // Remove avatar from character data to avoid conflicts + const [updated] = await db + .update(schema.characters) + .set(data) + .where( + and( + eq(schema.characters.id, id), + eq(schema.characters.userId, userId) + ) + ) + .returning() - if (message.avatarFile) { - await handleCharacterAvatarUpload({ - character: updated, - avatarFile: message.avatarFile - }) - } + if (message.avatarFile) { + await handleCharacterAvatarUpload({ + character: updated, + avatarFile: message.avatarFile + }) + } - const res: Sockets.UpdateCharacter.Response = { character: updated } - await charactersList(socket, {}, emitToUser) - emitToUser("updateCharacter", res) + const res: Sockets.UpdateCharacter.Response = { character: updated } + await charactersList(socket, {}, emitToUser) + emitToUser("updateCharacter", res) } export async function deleteCharacter( - socket: any, - message: Sockets.DeleteCharacter.Call, - emitToUser: (event: string, data: any) => void + socket: any, + message: Sockets.DeleteCharacter.Call, + emitToUser: (event: string, data: any) => void ) { - const userId = 1 // Replace with actual userId - await db - .delete(schema.characters) - .where( - and(eq(schema.characters.id, message.characterId), eq(schema.characters.userId, userId)) - ) - await charactersList(socket, {}, emitToUser) - // Delete the character data directory if it exists - const avatarDir = getCharacterDataDir({ - characterId: message.characterId, - userId - }) - try { - await fsPromises.rmdir(avatarDir, { recursive: true }) - } catch (err) { - console.error("Error deleting character data directory:", err) - } - // Emit the delete event - const res: Sockets.DeleteCharacter.Response = { id: message.characterId } - await charactersList(socket, {}, emitToUser) - emitToUser("deleteCharacter", res) + const userId = 1 // Replace with actual userId + await db + .delete(schema.characters) + .where( + and( + eq(schema.characters.id, message.characterId), + eq(schema.characters.userId, userId) + ) + ) + await charactersList(socket, {}, emitToUser) + // Delete the character data directory if it exists + const avatarDir = getCharacterDataDir({ + characterId: message.characterId, + userId + }) + try { + await fsPromises.rmdir(avatarDir, { recursive: true }) + } catch (err) { + console.error("Error deleting character data directory:", err) + } + // Emit the delete event + const res: Sockets.DeleteCharacter.Response = { id: message.characterId } + await charactersList(socket, {}, emitToUser) + emitToUser("deleteCharacter", res) } export async function characterCardImport( - socket: any, - message: { file?: string }, - emitToUser: (event: string, data: any) => void + socket: any, + message: Sockets.CharacterCardImport.Call, + emitToUser: (event: string, data: any) => void ) { - const userId = 1 - let charaData: CharaImportMetadata - let base64 = message.file! - if (base64.startsWith("data:")) base64 = base64.split(",")[1] - const buffer = Buffer.from(base64, "base64") + const userId = 1 + let base64 = message.file! + if (base64.startsWith("data:")) base64 = base64.split(",")[1] + const buffer = Buffer.from(base64, "base64") + const card = await CharacterCard.from_file(buffer) - const chunks = extractChunks(buffer) + const v3Data = card.toSpecV3().data + const creationDate = + v3Data.creation_date && !isNaN(Number(v3Data.creation_date)) + ? new Date(Number(v3Data.creation_date)).toISOString() + : new Date().toISOString() + const data: InsertCharacter = { + userId, + name: v3Data.name || "Imported Character", + description: v3Data.description || "", + personality: v3Data.personality || "", + scenario: v3Data.scenario || "", + firstMessage: v3Data.first_mes || "", + exampleDialogues: v3Data.mes_example || "", + nickname: v3Data.nickname || "", + alternateGreetings: v3Data.alternate_greetings || [], + creatorNotes: v3Data.creator_notes || "", + creatorNotesMultilingual: v3Data.creator_notes_multilingual || {}, + groupOnlyGreetings: v3Data.group_only_greetings || [], + postHistoryInstructions: v3Data.post_history_instructions || "", + source: v3Data.source || [], + assets: v3Data.assets || [], + createdAt: creationDate, + extensions: v3Data.extensions || [] + } - for (const chunk of chunks) { - if (chunk.name === "tEXt") { - const { keyword, text } = decodeText(chunk.data) - if (keyword.toLocaleLowerCase() === "chara") { - charaData = JSON.parse( - Buffer.from(text, "base64").toString("utf8") - ) as CharaImportMetadata - } - } - } + const [character] = await db + .insert(schema.characters) + .values(data) + .returning() - const data: InsertCharacter = { - userId, - name: charaData!.data.name || "Imported Character", - description: charaData!.data.description || "", - personality: charaData!.data.personality || "", - scenario: charaData!.data.scenario || "", - firstMessage: charaData!.data.first_mes || "", - exampleDialogues: charaData!.data.mes_example || "", - } + // Extract file extension and check if it's a supported image type + let ext = "" + if (typeof message.file === "string") { + // If data URL, try to extract extension from MIME type + const dataUrlMatch = message.file.match(/^data:image\/(\w+)/i) + if (dataUrlMatch) { + ext = dataUrlMatch[1].toLowerCase() + } else { + // Otherwise, try to extract from filename + const fileNameMatch = message.file.match(/\.([a-zA-Z0-9]+)$/) + if (fileNameMatch) { + ext = fileNameMatch[1].toLowerCase() + } + } + } - const [character] = await db.insert(schema.characters).values(data).returning() - await handleCharacterAvatarUpload({ - character, - avatarFile: buffer - }) - const res: Sockets.CreateCharacter.Response = { character } - emitToUser("createCharacter", res) - await charactersList(socket, {}, emitToUser) -} \ No newline at end of file + async function detectMimeType(base64: string) { + const buffer = Buffer.from(base64, "base64") + const result = await fileTypeFromBuffer(buffer) + return result ? result.mime : null + } + + const mimeType = await detectMimeType(base64) + + console.log("Extracted mime type:", ) + const supportedMimeTypes = [ + "image/png", + "image/apng", + ] + if (supportedMimeTypes.includes(mimeType || "")) { + await handleCharacterAvatarUpload({ + character, + avatarFile: buffer + }) + } + + // TODO: Import tags + // TODO: Import lorebook + + const res: Sockets.CharacterCardImport.Response = { character } + emitToUser("createCharacter", res) + await charactersList(socket, {}, emitToUser) +} diff --git a/src/routes/chats/[id]/+page.svelte b/src/routes/chats/[id]/+page.svelte index 9e5f839..332974c 100644 --- a/src/routes/chats/[id]/+page.svelte +++ b/src/routes/chats/[id]/+page.svelte @@ -250,14 +250,15 @@ {character?.name || "Unknown"}{character?.nickname || character?.name || "Unknown"} {#if editChatMessage && editChatMessage.id === msg.id} @@ -377,6 +378,7 @@ size="w-[4em] h-[4em]" name={persona?.name ?? "Unknown"} background="preset-filled-primary-500" + imageClasses="object-cover" > diff --git a/src/routes/images/[...reqPath]/+server.ts b/src/routes/images/[...reqPath]/+server.ts index 8dcd691..3d9f550 100644 --- a/src/routes/images/[...reqPath]/+server.ts +++ b/src/routes/images/[...reqPath]/+server.ts @@ -13,7 +13,6 @@ export const GET: RequestHandler = async ({ params }) => { const relPath = Array.isArray(reqPath) ? reqPath.join('/') : reqPath const appData = envPaths('SerenePub', { suffix: "" }).data const filePath = path.join(appData, relPath) - console.log(`Serving avatar from: ${filePath}`) try { const data = await fs.readFile(filePath) // Guess content type from extension @@ -25,7 +24,7 @@ export const GET: RequestHandler = async ({ params }) => { else if (ext === '.gif') type = 'image/gif' // SvelteKit Response expects Uint8Array, not Buffer return new Response(new Uint8Array(data), { - headers: { 'Content-Type': type, 'Cache-Control': 'public, max-age=31536000' } + headers: { 'Content-Type': type, 'Cache-Control': 'public, max-age=0' } }) } catch (e) { return new Response('Not found', { status: 404 })