mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2025-04-13 22:02:58 +00:00
new
This commit is contained in:
parent
456e0a8da7
commit
b37fbf6c41
@ -1,3 +1,7 @@
|
||||
# LoLLMs v19.2242 (tWINS) Changelog
|
||||
Date: April 09, 2025
|
||||
- Added the possibility to star discussions
|
||||
|
||||
# LoLLMs v19.10 (Ghibli) Changelog
|
||||
Date: April 02, 2025
|
||||
|
||||
|
@ -1 +1 @@
|
||||
import{b as O,d as b,L as r,f as s,g as a,s as t,j as P,l as n,t as e}from"./index-BcINHkQK.js";const S={__proto__:null,anyref:34,dataref:34,eqref:34,externref:34,i31ref:34,funcref:34,i8:34,i16:34,i32:34,i64:34,f32:34,f64:34},Q=r.deserialize({version:14,states:"!^Q]QPOOOqQPO'#CbOOQO'#Cd'#CdOOQO'#Cl'#ClOOQO'#Ch'#ChQ]QPOOOOQO,58|,58|OxQPO,58|OOQO-E6f-E6fOOQO1G.h1G.h",stateData:"!P~O_OSPOSQOS~OTPOVROXROYROZROaQO~OSUO~P]OSXO~P]O",goto:"xaPPPPPPbPbPPPhPPPrXROPTVQTOQVPTWTVXSOPTV",nodeNames:"⚠ LineComment BlockComment Module ) ( App Identifier Type Keyword Number String",maxTerm:17,nodeProps:[["isolate",-3,1,2,11,""],["openedBy",4,"("],["closedBy",5,")"],["group",-6,6,7,8,9,10,11,"Expression"]],skippedNodes:[0,1,2],repeatNodeCount:1,tokenData:"0o~R^XY}YZ}]^}pq}rs!Stu#pxy'Uyz(e{|(j}!O(j!Q!R(s!R![*p!]!^.^#T#o.{~!SO_~~!VVOr!Srs!ls#O!S#O#P!q#P;'S!S;'S;=`#j<%lO!S~!qOZ~~!tRO;'S!S;'S;=`!};=`O!S~#QWOr!Srs!ls#O!S#O#P!q#P;'S!S;'S;=`#j;=`<%l!S<%lO!S~#mP;=`<%l!S~#siqr%bst%btu%buv%bvw%bwx%bz{%b{|%b}!O%b!O!P%b!P!Q%b!Q![%b![!]%b!^!_%b!_!`%b!`!a%b!a!b%b!b!c%b!c!}%b#Q#R%b#R#S%b#S#T%b#T#o%b#p#q%b#r#s%b~%giV~qr%bst%btu%buv%bvw%bwx%bz{%b{|%b}!O%b!O!P%b!P!Q%b!Q![%b![!]%b!^!_%b!_!`%b!`!a%b!a!b%b!b!c%b!c!}%b#Q#R%b#R#S%b#S#T%b#T#o%b#p#q%b#r#s%b~'ZPT~!]!^'^~'aTO!]'^!]!^'p!^;'S'^;'S;=`(_<%lO'^~'sVOy'^yz(Yz!]'^!]!^'p!^;'S'^;'S;=`(_<%lO'^~(_OQ~~(bP;=`<%l'^~(jOS~~(mQ!Q!R(s!R![*p~(xUY~!O!P)[!Q![*p!g!h){#R#S+U#X#Y){#l#m+[~)aRY~!Q![)j!g!h){#X#Y){~)oSY~!Q![)j!g!h){#R#S*j#X#Y){~*OR{|*X}!O*X!Q![*_~*[P!Q![*_~*dQY~!Q![*_#R#S*X~*mP!Q![)j~*uTY~!O!P)[!Q![*p!g!h){#R#S+U#X#Y){~+XP!Q![*p~+_R!Q![+h!c!i+h#T#Z+h~+mVY~!O!P,S!Q![+h!c!i+h!r!s-P#R#S+[#T#Z+h#d#e-P~,XTY~!Q![,h!c!i,h!r!s-P#T#Z,h#d#e-P~,mUY~!Q![,h!c!i,h!r!s-P#R#S.Q#T#Z,h#d#e-P~-ST{|-c}!O-c!Q![-o!c!i-o#T#Z-o~-fR!Q![-o!c!i-o#T#Z-o~-tSY~!Q![-o!c!i-o#R#S-c#T#Z-o~.TR!Q![,h!c!i,h#T#Z,h~.aP!]!^.d~.iSP~OY.dZ;'S.d;'S;=`.u<%lO.d~.xP;=`<%l.d~/QiX~qr.{st.{tu.{uv.{vw.{wx.{z{.{{|.{}!O.{!O!P.{!P!Q.{!Q![.{![!].{!^!_.{!_!`.{!`!a.{!a!b.{!b!c.{!c!}.{#Q#R.{#R#S.{#S#T.{#T#o.{#p#q.{#r#s.{",tokenizers:[0],topRules:{Module:[0,3]},specialized:[{term:9,get:o=>S[o]||-1}],tokenPrec:0}),i=O.define({name:"wast",parser:Q.configure({props:[s.add({App:P({closing:")",align:!1})}),a.add({App:n,BlockComment(o){return{from:o.from+2,to:o.to-2}}}),t({Keyword:e.keyword,Type:e.typeName,Number:e.number,String:e.string,Identifier:e.variableName,LineComment:e.lineComment,BlockComment:e.blockComment,"( )":e.paren})]}),languageData:{commentTokens:{line:";;",block:{open:"(;",close:";)"}},closeBrackets:{brackets:["(",'"']}}});function p(){return new b(i)}export{p as wast,i as wastLanguage};
|
||||
import{b as O,d as b,L as r,f as s,g as a,s as t,j as P,l as n,t as e}from"./index-Cj8ZvKDR.js";const S={__proto__:null,anyref:34,dataref:34,eqref:34,externref:34,i31ref:34,funcref:34,i8:34,i16:34,i32:34,i64:34,f32:34,f64:34},Q=r.deserialize({version:14,states:"!^Q]QPOOOqQPO'#CbOOQO'#Cd'#CdOOQO'#Cl'#ClOOQO'#Ch'#ChQ]QPOOOOQO,58|,58|OxQPO,58|OOQO-E6f-E6fOOQO1G.h1G.h",stateData:"!P~O_OSPOSQOS~OTPOVROXROYROZROaQO~OSUO~P]OSXO~P]O",goto:"xaPPPPPPbPbPPPhPPPrXROPTVQTOQVPTWTVXSOPTV",nodeNames:"⚠ LineComment BlockComment Module ) ( App Identifier Type Keyword Number String",maxTerm:17,nodeProps:[["isolate",-3,1,2,11,""],["openedBy",4,"("],["closedBy",5,")"],["group",-6,6,7,8,9,10,11,"Expression"]],skippedNodes:[0,1,2],repeatNodeCount:1,tokenData:"0o~R^XY}YZ}]^}pq}rs!Stu#pxy'Uyz(e{|(j}!O(j!Q!R(s!R![*p!]!^.^#T#o.{~!SO_~~!VVOr!Srs!ls#O!S#O#P!q#P;'S!S;'S;=`#j<%lO!S~!qOZ~~!tRO;'S!S;'S;=`!};=`O!S~#QWOr!Srs!ls#O!S#O#P!q#P;'S!S;'S;=`#j;=`<%l!S<%lO!S~#mP;=`<%l!S~#siqr%bst%btu%buv%bvw%bwx%bz{%b{|%b}!O%b!O!P%b!P!Q%b!Q![%b![!]%b!^!_%b!_!`%b!`!a%b!a!b%b!b!c%b!c!}%b#Q#R%b#R#S%b#S#T%b#T#o%b#p#q%b#r#s%b~%giV~qr%bst%btu%buv%bvw%bwx%bz{%b{|%b}!O%b!O!P%b!P!Q%b!Q![%b![!]%b!^!_%b!_!`%b!`!a%b!a!b%b!b!c%b!c!}%b#Q#R%b#R#S%b#S#T%b#T#o%b#p#q%b#r#s%b~'ZPT~!]!^'^~'aTO!]'^!]!^'p!^;'S'^;'S;=`(_<%lO'^~'sVOy'^yz(Yz!]'^!]!^'p!^;'S'^;'S;=`(_<%lO'^~(_OQ~~(bP;=`<%l'^~(jOS~~(mQ!Q!R(s!R![*p~(xUY~!O!P)[!Q![*p!g!h){#R#S+U#X#Y){#l#m+[~)aRY~!Q![)j!g!h){#X#Y){~)oSY~!Q![)j!g!h){#R#S*j#X#Y){~*OR{|*X}!O*X!Q![*_~*[P!Q![*_~*dQY~!Q![*_#R#S*X~*mP!Q![)j~*uTY~!O!P)[!Q![*p!g!h){#R#S+U#X#Y){~+XP!Q![*p~+_R!Q![+h!c!i+h#T#Z+h~+mVY~!O!P,S!Q![+h!c!i+h!r!s-P#R#S+[#T#Z+h#d#e-P~,XTY~!Q![,h!c!i,h!r!s-P#T#Z,h#d#e-P~,mUY~!Q![,h!c!i,h!r!s-P#R#S.Q#T#Z,h#d#e-P~-ST{|-c}!O-c!Q![-o!c!i-o#T#Z-o~-fR!Q![-o!c!i-o#T#Z-o~-tSY~!Q![-o!c!i-o#R#S-c#T#Z-o~.TR!Q![,h!c!i,h#T#Z,h~.aP!]!^.d~.iSP~OY.dZ;'S.d;'S;=`.u<%lO.d~.xP;=`<%l.d~/QiX~qr.{st.{tu.{uv.{vw.{wx.{z{.{{|.{}!O.{!O!P.{!P!Q.{!Q![.{![!].{!^!_.{!_!`.{!`!a.{!a!b.{!b!c.{!c!}.{#Q#R.{#R#S.{#S#T.{#T#o.{#p#q.{#r#s.{",tokenizers:[0],topRules:{Module:[0,3]},specialized:[{term:9,get:o=>S[o]||-1}],tokenPrec:0}),i=O.define({name:"wast",parser:Q.configure({props:[s.add({App:P({closing:")",align:!1})}),a.add({App:n,BlockComment(o){return{from:o.from+2,to:o.to-2}}}),t({Keyword:e.keyword,Type:e.typeName,Number:e.number,String:e.string,Identifier:e.variableName,LineComment:e.lineComment,BlockComment:e.blockComment,"( )":e.paren})]}),languageData:{commentTokens:{line:";;",block:{open:"(;",close:";)"}},closeBrackets:{brackets:["(",'"']}}});function p(){return new b(i)}export{p as wast,i as wastLanguage};
|
File diff suppressed because one or more lines are too long
26
web/dist/assets/index-B_pLDTYj.css
vendored
Normal file
26
web/dist/assets/index-B_pLDTYj.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
26
web/dist/assets/index-BxPTDKFo.css
vendored
26
web/dist/assets/index-BxPTDKFo.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
import{b as q,d as P,q as g,r as l,L as c,u as i,s as R,t as r,E as p}from"./index-BcINHkQK.js";const b=1,$=33,m=34,v=35,x=36,d=new p(O=>{let e=O.pos;for(;;){if(O.next==10){O.advance();break}else if(O.next==123&&O.peek(1)==123||O.next<0)break;O.advance()}O.pos>e&&O.acceptToken(b)});function n(O,e,a){return new p(t=>{let u=t.pos;for(;t.next!=O&&t.next>=0&&(a||t.next!=38&&(t.next!=123||t.peek(1)!=123));)t.advance();t.pos>u&&t.acceptToken(e)})}const W=n(39,$,!1),C=n(34,m,!1),T=n(39,v,!0),f=n(34,x,!0),A=c.deserialize({version:14,states:"(jOVOqOOOeQpOOOvO!bO'#CaOOOP'#Cx'#CxQVOqOOO!OQpO'#CfO!WQpO'#ClO!]QpO'#CrO!bQpO'#CsOOQO'#Cv'#CvQ!gQpOOQ!lQpOOQ!qQpOOOOOV,58{,58{O!vOpO,58{OOOP-E6v-E6vO!{QpO,59QO#TQpO,59QOOQO,59W,59WO#YQpO,59^OOQO,59_,59_O#_QpOOO#_QpOOO#gQpOOOOOV1G.g1G.gO#oQpO'#CyO#tQpO1G.lOOQO1G.l1G.lO#|QpO1G.lOOQO1G.x1G.xO$UO`O'#DUO$ZOWO'#DUOOQO'#Co'#CoQOQpOOOOQO'#Cu'#CuO$`OtO'#CwO$qOrO'#CwOOQO,59e,59eOOQO-E6w-E6wOOQO7+$W7+$WO%SQpO7+$WO%[QpO7+$WOOOO'#Cp'#CpO%aOpO,59pOOOO'#Cq'#CqO%fOpO,59pOOOS'#Cz'#CzO%kOtO,59cOOQO,59c,59cOOOQ'#C{'#C{O%|OrO,59cO&_QpO<<GrOOQO<<Gr<<GrOOQO1G/[1G/[OOOS-E6x-E6xOOQO1G.}1G.}OOOQ-E6y-E6yOOQOAN=^AN=^",stateData:"&d~OvOS~OPROSQOVROWRO~OZTO[XO^VOaUOhWO~OR]OU^O~O[`O^aO~O[bO~O[cO~O[dO~ObeO~ObfO~ObgO~ORhO~O]kOwiO~O[lO~O_mO~OynOzoO~OysOztO~O[uO~O]wOwiO~O_yOwiO~OtzO~Os|O~OSQOV!OOW!OOr!OOy!QO~OSQOV!ROW!ROq!ROz!QO~O_!TOwiO~O]!UO~Oy!VO~Oz!VO~OSQOV!OOW!OOr!OOy!XO~OSQOV!ROW!ROq!ROz!XO~O]!ZO~O",goto:"#dyPPPPPzPPPP!WPPPPP!WPP!Z!^!a!d!dP!g!j!m!p!v#Q#WPPPPPPPP#^SROSS!Os!PT!Rt!SRYPRqeR{nR}oRZPRqfR[PRqgQSOR_SQj`SvjxRxlQ!PsR!W!PQ!StR!Y!SQpeRrf",nodeNames:"⚠ Text Content }} {{ Interpolation InterpolationContent Entity InvalidEntity Attribute BoundAttributeName [ Identifier ] ( ) ReferenceName # Is ExpressionAttributeValue AttributeInterpolation AttributeInterpolation EventName DirectiveName * StatementAttributeValue AttributeName AttributeValue",maxTerm:42,nodeProps:[["openedBy",3,"{{",15,"("],["closedBy",4,"}}",14,")"],["isolate",-4,5,19,25,27,""]],skippedNodes:[0],repeatNodeCount:4,tokenData:"0r~RyOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mqr#rrs%jst&Qtv#rvw&hwx)zxy*byz*xz{+`{}#r}!O+v!O!P-]!P!Q#r!Q![+v![!]+v!]!_#r!_!`-s!`!c#r!c!}+v!}#O.Z#O#P#r#P#Q.q#Q#R#r#R#S+v#S#T#r#T#o+v#o#p/X#p#q#r#q#r0Z#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vQ#wTUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rQ$ZSO#q#r#r;'S#r;'S;=`$g<%lO#rQ$jP;=`<%l#rR$t[UQvPOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mq#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR%qTyPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&XTaPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&oXUQWPOp'[pq#rq!]'[!]!^#r!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R'aXUQOp'[pq#rq!]'[!]!^'|!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R(TTVPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR(gXOp'[pq#rq!]'[!]!^'|!^#q'[#q#r)S#r;'S'[;'S;=`)t<%lO'[P)VUOp)Sq!])S!]!^)i!^;'S)S;'S;=`)n<%lO)SP)nOVPP)qP;=`<%l)SR)wP;=`<%l'[R*RTzPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR*iT^PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+PT_PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+gThPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+}b[PUQO}#r}!O+v!O!Q#r!Q![+v![!]+v!]!c#r!c!}+v!}#R#r#R#S+v#S#T#r#T#o+v#o#q#r#q#r$W#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vR-YP;=`<%l+vR-dTwPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR-zTUQbPO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.bTZPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.xT]PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/^VUQO#o#r#o#p/s#p#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/zTSPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#r~0^TO#q#r#q#r0m#r;'S#r;'S;=`$g<%lO#r~0rOR~",tokenizers:[d,W,C,T,f,0,1],topRules:{Content:[0,2],Attribute:[1,9]},tokenPrec:0}),V=i.parser.configure({top:"SingleExpression"}),Q=A.configure({props:[R({Text:r.content,Is:r.definitionOperator,AttributeName:r.attributeName,"AttributeValue ExpressionAttributeValue StatementAttributeValue":r.attributeValue,Entity:r.character,InvalidEntity:r.invalid,"BoundAttributeName/Identifier":r.attributeName,"EventName/Identifier":r.special(r.attributeName),"ReferenceName/Identifier":r.variableName,"DirectiveName/Identifier":r.keyword,"{{ }}":r.brace,"( )":r.paren,"[ ]":r.bracket,"# '*'":r.punctuation})]}),o={parser:V},w={parser:i.parser},U=Q.configure({wrap:l((O,e)=>O.name=="InterpolationContent"?o:null)}),y=Q.configure({wrap:l((O,e)=>{var a;return O.name=="InterpolationContent"?o:O.name!="AttributeInterpolation"?null:((a=O.node.parent)===null||a===void 0?void 0:a.name)=="StatementAttributeValue"?w:o}),top:"Attribute"}),E={parser:U},N={parser:y},s=g();function S(O){return O.configure({wrap:l(z)},"angular")}const k=S(s.language);function z(O,e){switch(O.name){case"Attribute":return/^[*#(\[]|\{\{/.test(e.read(O.from,O.to))?N:null;case"Text":return E}return null}function G(O={}){let e=s;if(O.base){if(O.base.language.name!="html"||!(O.base.language instanceof q))throw new RangeError("The base option must be the result of calling html(...)");e=O.base}return new P(e.language==s.language?k:S(e.language),[e.support,e.language.data.of({closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/})])}export{G as angular,k as angularLanguage};
|
||||
import{b as q,d as P,q as g,r as l,L as c,u as i,s as R,t as r,E as p}from"./index-Cj8ZvKDR.js";const b=1,$=33,m=34,v=35,x=36,d=new p(O=>{let e=O.pos;for(;;){if(O.next==10){O.advance();break}else if(O.next==123&&O.peek(1)==123||O.next<0)break;O.advance()}O.pos>e&&O.acceptToken(b)});function n(O,e,a){return new p(t=>{let u=t.pos;for(;t.next!=O&&t.next>=0&&(a||t.next!=38&&(t.next!=123||t.peek(1)!=123));)t.advance();t.pos>u&&t.acceptToken(e)})}const W=n(39,$,!1),C=n(34,m,!1),T=n(39,v,!0),f=n(34,x,!0),A=c.deserialize({version:14,states:"(jOVOqOOOeQpOOOvO!bO'#CaOOOP'#Cx'#CxQVOqOOO!OQpO'#CfO!WQpO'#ClO!]QpO'#CrO!bQpO'#CsOOQO'#Cv'#CvQ!gQpOOQ!lQpOOQ!qQpOOOOOV,58{,58{O!vOpO,58{OOOP-E6v-E6vO!{QpO,59QO#TQpO,59QOOQO,59W,59WO#YQpO,59^OOQO,59_,59_O#_QpOOO#_QpOOO#gQpOOOOOV1G.g1G.gO#oQpO'#CyO#tQpO1G.lOOQO1G.l1G.lO#|QpO1G.lOOQO1G.x1G.xO$UO`O'#DUO$ZOWO'#DUOOQO'#Co'#CoQOQpOOOOQO'#Cu'#CuO$`OtO'#CwO$qOrO'#CwOOQO,59e,59eOOQO-E6w-E6wOOQO7+$W7+$WO%SQpO7+$WO%[QpO7+$WOOOO'#Cp'#CpO%aOpO,59pOOOO'#Cq'#CqO%fOpO,59pOOOS'#Cz'#CzO%kOtO,59cOOQO,59c,59cOOOQ'#C{'#C{O%|OrO,59cO&_QpO<<GrOOQO<<Gr<<GrOOQO1G/[1G/[OOOS-E6x-E6xOOQO1G.}1G.}OOOQ-E6y-E6yOOQOAN=^AN=^",stateData:"&d~OvOS~OPROSQOVROWRO~OZTO[XO^VOaUOhWO~OR]OU^O~O[`O^aO~O[bO~O[cO~O[dO~ObeO~ObfO~ObgO~ORhO~O]kOwiO~O[lO~O_mO~OynOzoO~OysOztO~O[uO~O]wOwiO~O_yOwiO~OtzO~Os|O~OSQOV!OOW!OOr!OOy!QO~OSQOV!ROW!ROq!ROz!QO~O_!TOwiO~O]!UO~Oy!VO~Oz!VO~OSQOV!OOW!OOr!OOy!XO~OSQOV!ROW!ROq!ROz!XO~O]!ZO~O",goto:"#dyPPPPPzPPPP!WPPPPP!WPP!Z!^!a!d!dP!g!j!m!p!v#Q#WPPPPPPPP#^SROSS!Os!PT!Rt!SRYPRqeR{nR}oRZPRqfR[PRqgQSOR_SQj`SvjxRxlQ!PsR!W!PQ!StR!Y!SQpeRrf",nodeNames:"⚠ Text Content }} {{ Interpolation InterpolationContent Entity InvalidEntity Attribute BoundAttributeName [ Identifier ] ( ) ReferenceName # Is ExpressionAttributeValue AttributeInterpolation AttributeInterpolation EventName DirectiveName * StatementAttributeValue AttributeName AttributeValue",maxTerm:42,nodeProps:[["openedBy",3,"{{",15,"("],["closedBy",4,"}}",14,")"],["isolate",-4,5,19,25,27,""]],skippedNodes:[0],repeatNodeCount:4,tokenData:"0r~RyOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mqr#rrs%jst&Qtv#rvw&hwx)zxy*byz*xz{+`{}#r}!O+v!O!P-]!P!Q#r!Q![+v![!]+v!]!_#r!_!`-s!`!c#r!c!}+v!}#O.Z#O#P#r#P#Q.q#Q#R#r#R#S+v#S#T#r#T#o+v#o#p/X#p#q#r#q#r0Z#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vQ#wTUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rQ$ZSO#q#r#r;'S#r;'S;=`$g<%lO#rQ$jP;=`<%l#rR$t[UQvPOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mq#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR%qTyPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&XTaPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&oXUQWPOp'[pq#rq!]'[!]!^#r!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R'aXUQOp'[pq#rq!]'[!]!^'|!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R(TTVPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR(gXOp'[pq#rq!]'[!]!^'|!^#q'[#q#r)S#r;'S'[;'S;=`)t<%lO'[P)VUOp)Sq!])S!]!^)i!^;'S)S;'S;=`)n<%lO)SP)nOVPP)qP;=`<%l)SR)wP;=`<%l'[R*RTzPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR*iT^PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+PT_PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+gThPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+}b[PUQO}#r}!O+v!O!Q#r!Q![+v![!]+v!]!c#r!c!}+v!}#R#r#R#S+v#S#T#r#T#o+v#o#q#r#q#r$W#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vR-YP;=`<%l+vR-dTwPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR-zTUQbPO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.bTZPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.xT]PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/^VUQO#o#r#o#p/s#p#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/zTSPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#r~0^TO#q#r#q#r0m#r;'S#r;'S;=`$g<%lO#r~0rOR~",tokenizers:[d,W,C,T,f,0,1],topRules:{Content:[0,2],Attribute:[1,9]},tokenPrec:0}),V=i.parser.configure({top:"SingleExpression"}),Q=A.configure({props:[R({Text:r.content,Is:r.definitionOperator,AttributeName:r.attributeName,"AttributeValue ExpressionAttributeValue StatementAttributeValue":r.attributeValue,Entity:r.character,InvalidEntity:r.invalid,"BoundAttributeName/Identifier":r.attributeName,"EventName/Identifier":r.special(r.attributeName),"ReferenceName/Identifier":r.variableName,"DirectiveName/Identifier":r.keyword,"{{ }}":r.brace,"( )":r.paren,"[ ]":r.bracket,"# '*'":r.punctuation})]}),o={parser:V},w={parser:i.parser},U=Q.configure({wrap:l((O,e)=>O.name=="InterpolationContent"?o:null)}),y=Q.configure({wrap:l((O,e)=>{var a;return O.name=="InterpolationContent"?o:O.name!="AttributeInterpolation"?null:((a=O.node.parent)===null||a===void 0?void 0:a.name)=="StatementAttributeValue"?w:o}),top:"Attribute"}),E={parser:U},N={parser:y},s=g();function S(O){return O.configure({wrap:l(z)},"angular")}const k=S(s.language);function z(O,e){switch(O.name){case"Attribute":return/^[*#(\[]|\{\{/.test(e.read(O.from,O.to))?N:null;case"Text":return E}return null}function G(O={}){let e=s;if(O.base){if(O.base.language.name!="html"||!(O.base.language instanceof q))throw new RangeError("The base option must be the result of calling html(...)");e=O.base}return new P(e.language==s.language?k:S(e.language),[e.support,e.language.data.of({closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/})])}export{G as angular,k as angularLanguage};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
web/dist/index.html
vendored
4
web/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LoLLMS WebUI</title>
|
||||
<script type="module" crossorigin src="/assets/index-BcINHkQK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BxPTDKFo.css">
|
||||
<script type="module" crossorigin src="/assets/index-Cj8ZvKDR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B_pLDTYj.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@ -87,7 +87,7 @@ export default {
|
||||
showAudioViewer: false,
|
||||
// Style Management
|
||||
injectedStyleElements: [],
|
||||
renderCount: 0,
|
||||
renderCount: 0, // Added for detailed logging
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -102,335 +102,475 @@ export default {
|
||||
handler(newValue, oldValue) {
|
||||
this.renderCount++;
|
||||
const currentRender = this.renderCount;
|
||||
console.log(`[${this.instanceId}] Watcher triggered (Render #${currentRender}). UI changed: ${newValue !== oldValue}.`);
|
||||
console.log(`[${this.instanceId}] Watcher triggered (Render #${currentRender}). UI changed: ${newValue !== oldValue}. New UI length: ${newValue?.length ?? 0}`);
|
||||
|
||||
// Check if the container ref exists before evaluating if it needs render based on child nodes
|
||||
const htmlContainer = this.$refs.htmlContentContainer;
|
||||
// Ensure render happens if value changes OR if the container is somehow empty (e.g., HMR)
|
||||
const needsRender = newValue !== oldValue || !htmlContainer || !htmlContainer.hasChildNodes();
|
||||
console.log(`[${this.instanceId}] Needs render evaluation (Render #${currentRender}): ${needsRender}`);
|
||||
const needsRenderBasedOnContent = !htmlContainer || !htmlContainer.hasChildNodes();
|
||||
|
||||
// Determine if a render is needed: either the value changed OR the container is unexpectedly empty.
|
||||
const needsRender = newValue !== oldValue || needsRenderBasedOnContent;
|
||||
console.log(`[${this.instanceId}] Needs render evaluation (Render #${currentRender}): Value Changed=${newValue !== oldValue}, Container Empty=${needsRenderBasedOnContent} => Needs Render=${needsRender}`);
|
||||
|
||||
if (needsRender) {
|
||||
console.log(`[${this.instanceId}] --- Starting Update Cycle (Render #${currentRender}) ---`);
|
||||
this.cleanupDynamicContent(currentRender);
|
||||
// Pass render context to cleanup for better logging
|
||||
this.cleanupDynamicContent(`before render #${currentRender}`);
|
||||
|
||||
this.$nextTick(() => {
|
||||
console.log(`[${this.instanceId}] $nextTick after cleanup (Render #${currentRender}): Starting renderContent.`);
|
||||
// Pass render context to renderContent
|
||||
this.renderContent(currentRender);
|
||||
});
|
||||
} else {
|
||||
console.log(`[${this.instanceId}] Watcher triggered but skipping render (Render #${currentRender}).`);
|
||||
console.log(`[${this.instanceId}] Watcher triggered but skipping render (Render #${currentRender}). UI appears unchanged and container has content.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
console.log(`[${this.instanceId}] Component beforeUnmount hook.`);
|
||||
// Pass context to cleanup
|
||||
this.cleanupDynamicContent('beforeUnmount');
|
||||
},
|
||||
methods: {
|
||||
logAlbumViewerMounted(type) {
|
||||
console.log(`[${this.instanceId}] ${type} AlbumViewer successfully MOUNTED.`);
|
||||
// Log when the conditional viewers actually get mounted
|
||||
console.log(`%c[${this.instanceId}] ${type} AlbumViewer successfully MOUNTED.`, 'color: green; font-weight: bold;');
|
||||
},
|
||||
|
||||
renderContent(renderContext) {
|
||||
console.log(`[${this.instanceId}] renderContent CALLED (Context: ${renderContext})`);
|
||||
console.log(`%c[${this.instanceId}] renderContent CALLED (Context: Render #${renderContext})`, 'color: blue; font-weight: bold;');
|
||||
const targetContainer = this.$refs.htmlContentContainer;
|
||||
if (!targetContainer) {
|
||||
console.error(`[${this.instanceId}] ERROR: htmlContentContainer ref NOT FOUND during renderContent (Context: ${renderContext})!`);
|
||||
// This is a critical error for rendering
|
||||
console.error(`[${this.instanceId}] CRITICAL ERROR: htmlContentContainer ref NOT FOUND during renderContent (Context: Render #${renderContext})! Cannot render HTML.`);
|
||||
return;
|
||||
}
|
||||
|
||||
targetContainer.innerHTML = ''; // Ensure clean slate
|
||||
console.log(`[${this.instanceId}] Cleared targetContainer innerHTML (Context: ${renderContext})`);
|
||||
// Clear previous standard HTML content *before* processing new UI
|
||||
targetContainer.innerHTML = '';
|
||||
console.log(`[${this.instanceId}] Cleared targetContainer innerHTML (Context: Render #${renderContext})`);
|
||||
|
||||
let imagesForMedia = []; // Local collection for images
|
||||
let videosForMedia = []; // Local collection for videos
|
||||
let audiosForMedia = []; // Local collection for audios
|
||||
let mediaPlaceholderNeeded = false; // Flag if any media viewer might be needed
|
||||
// Initialize local collections for media elements found in this render cycle
|
||||
let imagesForMedia = [];
|
||||
let videosForMedia = [];
|
||||
let audiosForMedia = [];
|
||||
let mediaPlaceholderNeeded = false; // Flag to check if *any* media viewer might be needed later
|
||||
|
||||
const incomingUi = this.ui || '';
|
||||
const incomingUi = this.ui || ''; // Ensure we have a string
|
||||
|
||||
// Handle empty UI string explicitly
|
||||
if (!incomingUi.trim()) {
|
||||
console.warn(`[${this.instanceId}] No UI content provided (Context: ${renderContext}). Skipping parsing.`);
|
||||
this.resetMediaState(`empty UI (${renderContext})`); // Ensure state is reset
|
||||
return;
|
||||
console.warn(`[${this.instanceId}] No UI content provided or UI is empty whitespace (Context: Render #${renderContext}). Skipping parsing and rendering.`);
|
||||
// Explicitly reset media state if UI is empty
|
||||
this.resetMediaState(`empty UI received (Render #${renderContext})`);
|
||||
return; // Stop processing if there's nothing to render
|
||||
}
|
||||
|
||||
console.log(`[${this.instanceId}] Parsing UI content (length: ${incomingUi.length}) (Context: ${renderContext})`);
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(incomingUi, 'text/html');
|
||||
console.log(`[${this.instanceId}] Parsing UI content (length: ${incomingUi.length}) (Context: Render #${renderContext})`);
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(incomingUi, 'text/html');
|
||||
|
||||
// --- 1. Inject Scoped CSS ---
|
||||
const styles = doc.head.querySelectorAll('style');
|
||||
console.log(`[${this.instanceId}] Found ${styles.length} style tags in <head>.`);
|
||||
styles.forEach((style, index) => {
|
||||
console.log(`[${this.instanceId}] Injecting head style #${index + 1}.`);
|
||||
this.injectScopedCss(style.textContent, renderContext);
|
||||
});
|
||||
const bodyStyles = doc.body.querySelectorAll('style');
|
||||
console.log(`[${this.instanceId}] Found ${bodyStyles.length} style tags in <body>.`);
|
||||
bodyStyles.forEach((style, index) => {
|
||||
console.log(`[${this.instanceId}] Injecting body style #${index + 1}.`);
|
||||
this.injectScopedCss(style.textContent, renderContext);
|
||||
});
|
||||
// --- 1. Inject Scoped CSS ---
|
||||
// Note: Styles are injected into the document <head>, not the component's container
|
||||
const styles = doc.head.querySelectorAll('style');
|
||||
console.log(`[${this.instanceId}] Found ${styles.length} style tags in parsed <head>.`);
|
||||
styles.forEach((style, index) => {
|
||||
console.log(`[${this.instanceId}] Injecting head style #${index + 1} (Context: Render #${renderContext}).`);
|
||||
this.injectScopedCss(style.textContent, `head style #${index + 1} / Render #${renderContext}`);
|
||||
});
|
||||
const bodyStyles = doc.body.querySelectorAll('style');
|
||||
console.log(`[${this.instanceId}] Found ${bodyStyles.length} style tags in parsed <body>.`);
|
||||
bodyStyles.forEach((style, index) => {
|
||||
console.log(`[${this.instanceId}] Injecting body style #${index + 1} (Context: Render #${renderContext}).`);
|
||||
this.injectScopedCss(style.textContent, `body style #${index + 1} / Render #${renderContext}`);
|
||||
});
|
||||
|
||||
// --- 2. Process HTML Body ---
|
||||
console.log(`[${this.instanceId}] Processing body childNodes (Context: ${renderContext})`);
|
||||
const processedNodes = []; // Nodes to be appended directly
|
||||
const nodesToProcess = Array.from(doc.body.childNodes);
|
||||
console.log(`[${this.instanceId}] Found ${nodesToProcess.length} nodes in parsed body.`);
|
||||
// --- 2. Process HTML Body Nodes ---
|
||||
console.log(`[${this.instanceId}] Processing body childNodes (Context: Render #${renderContext})`);
|
||||
const processedNodes = []; // Collect nodes to be appended to targetContainer
|
||||
const nodesToProcess = Array.from(doc.body.childNodes);
|
||||
console.log(`[${this.instanceId}] Found ${nodesToProcess.length} nodes in parsed body.`);
|
||||
|
||||
nodesToProcess.forEach((node, index) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const tagName = node.tagName.toUpperCase();
|
||||
const classList = node.classList;
|
||||
console.log(`[${this.instanceId}] Processing Node #${index + 1}: <${tagName}>, Classes: ${classList}`);
|
||||
nodesToProcess.forEach((node, index) => {
|
||||
const nodeIdentifier = `Node #${index + 1} (Type: ${node.nodeType})`;
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const tagName = node.tagName.toUpperCase();
|
||||
const classList = node.classList;
|
||||
console.log(`[${this.instanceId}] Processing ${nodeIdentifier}: <${tagName}>, Classes: [${Array.from(classList).join(', ')}]`);
|
||||
|
||||
// --- Special Handling: Media Elements (Image, Video, Audio) ---
|
||||
if (classList.contains('media')) {
|
||||
const src = node.getAttribute('src'); // Common attribute
|
||||
if (!src) {
|
||||
console.warn(`[${this.instanceId}] -> Found '<${tagName}.media>' but it has NO src attribute. Skipping.`);
|
||||
return; // Skip nodes without src
|
||||
}
|
||||
mediaPlaceholderNeeded = true; // Mark that some media viewer might be needed
|
||||
// --- Special Handling: Media Elements (Image, Video, Audio) ---
|
||||
if (classList.contains('media')) {
|
||||
const src = node.getAttribute('src'); // Common attribute for media
|
||||
if (!src) {
|
||||
// Warn if a media element lacks a source
|
||||
console.warn(`[${this.instanceId}] -> Found '<${tagName}.media>' but it has NO 'src' attribute. Skipping this media element.`);
|
||||
return; // Skip this specific node, don't add to processedNodes or media collections
|
||||
}
|
||||
mediaPlaceholderNeeded = true; // Mark that we found at least one valid media element
|
||||
|
||||
if (tagName === 'IMG') {
|
||||
console.log(`[${this.instanceId}] -> Found 'img.media' with src: ${src}`);
|
||||
imagesForMedia.push(src);
|
||||
return; // Don't append media elements directly, they go to viewers
|
||||
} else if (tagName === 'VIDEO') {
|
||||
console.log(`[${this.instanceId}] -> Found 'video.media' with src: ${src}`);
|
||||
videosForMedia.push(src);
|
||||
return; // Don't append media elements directly
|
||||
} else if (tagName === 'AUDIO') {
|
||||
console.log(`[${this.instanceId}] -> Found 'audio.media' with src: ${src}`);
|
||||
audiosForMedia.push(src);
|
||||
return; // Don't append media elements directly
|
||||
} else {
|
||||
console.warn(`[${this.instanceId}] -> Found '<${tagName}.media>' but it's not an IMG, VIDEO, or AUDIO tag. Treating as standard node.`);
|
||||
// Fall through to be added to processedNodes if not returned
|
||||
}
|
||||
if (tagName === 'IMG') {
|
||||
console.log(`[${this.instanceId}] -> Found 'img.media' with src: ${src}. Adding to image album list.`);
|
||||
imagesForMedia.push(src);
|
||||
// IMPORTANT: Do *not* add to processedNodes, handled by ImageAlbumViewer
|
||||
return;
|
||||
} else if (tagName === 'VIDEO') {
|
||||
console.log(`[${this.instanceId}] -> Found 'video.media' with src: ${src}. Adding to video album list.`);
|
||||
videosForMedia.push(src);
|
||||
// IMPORTANT: Do *not* add to processedNodes
|
||||
return;
|
||||
} else if (tagName === 'AUDIO') {
|
||||
console.log(`[${this.instanceId}] -> Found 'audio.media' with src: ${src}. Adding to audio album list.`);
|
||||
audiosForMedia.push(src);
|
||||
// IMPORTANT: Do *not* add to processedNodes
|
||||
return;
|
||||
} else {
|
||||
// Log if a non-standard tag has .media class
|
||||
console.warn(`[${this.instanceId}] -> Found '<${tagName}.media>' but it's not an IMG, VIDEO, or AUDIO tag. Treating as a standard HTML node.`);
|
||||
// Fall through to be added to processedNodes below
|
||||
}
|
||||
}
|
||||
|
||||
// --- Special Handling: Clickable Image POST ---
|
||||
if (tagName === 'IMG' && classList.contains('clickable-post')) {
|
||||
console.log(`[${this.instanceId}] -> Found 'img.clickable-post'. Ensuring data attributes are present (or defaults).`);
|
||||
// Ensure necessary data attributes exist for the click handler
|
||||
if (!node.dataset.endpoint) node.dataset.endpoint = '/post_to_personality'; // Default endpoint
|
||||
if (!node.dataset.payloadKey) node.dataset.payloadKey = 'img_path'; // Default payload key
|
||||
// Fall through to be added to processedNodes
|
||||
}
|
||||
|
||||
// --- Special Handling: Open Folder Link ---
|
||||
if (tagName === 'A' && classList.contains('open-folder')) {
|
||||
console.log(`[${this.instanceId}] -> Found 'a.open-folder'. Setting href='#' to prevent navigation.`);
|
||||
node.setAttribute('href', '#'); // Prevent default link navigation
|
||||
// Fall through to be added to processedNodes
|
||||
}
|
||||
|
||||
// --- Special Handling: Source Elements (Rendered as standard HTML) ---
|
||||
if (tagName === 'INTERNET_SOURCE' || tagName === 'LOCAL_SOURCE') {
|
||||
console.log(`[${this.instanceId}] -> Found '${tagName}'. Will be rendered directly as HTML.`);
|
||||
// Extract attributes for logging clarity, but mainly just render the element as is.
|
||||
const href = node.getAttribute('href');
|
||||
const summary = node.getAttribute('summary');
|
||||
const similarity = node.getAttribute('similarity');
|
||||
console.log(`[${this.instanceId}] Attributes - href: ${href}, summary: ${summary}, similarity: ${similarity}`);
|
||||
// Fall through to be added to processedNodes
|
||||
}
|
||||
|
||||
// If node hasn't been returned (like media elements), add it for standard HTML rendering
|
||||
processedNodes.push(node);
|
||||
console.log(`[${this.instanceId}] -> Adding <${tagName}> to processedNodes list for direct rendering.`);
|
||||
|
||||
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
|
||||
// Process non-empty text nodes
|
||||
console.log(`[${this.instanceId}] Processing ${nodeIdentifier}: TextNode (non-empty). Adding to processedNodes list.`);
|
||||
processedNodes.push(node);
|
||||
} else {
|
||||
// Log skipped nodes (like empty text nodes, comments)
|
||||
console.log(`[${this.instanceId}] Skipping ${nodeIdentifier}`);
|
||||
}
|
||||
});
|
||||
|
||||
// --- 3. Append Processed Standard HTML & Source Nodes ---
|
||||
console.log(`[${this.instanceId}] Appending ${processedNodes.length} processed nodes to targetContainer (Context: Render #${renderContext})`);
|
||||
if (processedNodes.length > 0) {
|
||||
processedNodes.forEach(node => {
|
||||
// Use importNode to ensure nodes are correctly adopted by the document
|
||||
targetContainer.appendChild(document.importNode(node, true));
|
||||
});
|
||||
console.log(`[${this.instanceId}] Finished appending nodes. Current targetContainer innerHTML length: ${targetContainer.innerHTML.length}`);
|
||||
} else {
|
||||
console.log(`[${this.instanceId}] No standard HTML nodes to append.`);
|
||||
}
|
||||
|
||||
// --- Special Handling: Clickable Image POST ---
|
||||
if (tagName === 'IMG' && classList.contains('clickable-post')) {
|
||||
console.log(`[${this.instanceId}] -> Found 'img.clickable-post'. Ensuring data attributes.`);
|
||||
if (!node.dataset.endpoint) node.dataset.endpoint = '/post_to_personality';
|
||||
if (!node.dataset.payloadKey) node.dataset.payloadKey = 'img_path';
|
||||
// Fall through to be added to processedNodes
|
||||
}
|
||||
// --- 4. Update State for Media Viewers ---
|
||||
// This happens *after* standard HTML is rendered
|
||||
console.log(`[${this.instanceId}] Evaluating media state updates (Context: Render #${renderContext}). Found media elements needing viewers: ${mediaPlaceholderNeeded}`);
|
||||
|
||||
// --- Special Handling: Open Folder Link ---
|
||||
if (tagName === 'A' && classList.contains('open-folder')) {
|
||||
console.log(`[${this.instanceId}] -> Found 'a.open-folder'. Setting href='#'`);
|
||||
node.setAttribute('href', '#'); // Prevent navigation
|
||||
// Fall through to be added to processedNodes
|
||||
}
|
||||
|
||||
// --- Special Handling: Source Elements ---
|
||||
if (tagName === 'INTERNET_SOURCE' || tagName === 'LOCAL_SOURCE') {
|
||||
console.log(`[${this.instanceId}] -> Found '${tagName}'. Will be rendered directly.`);
|
||||
// Extract attributes for potential future use, but mainly just render it.
|
||||
// Attributes: icon, href, summary, similarity
|
||||
// Fall through to be added to processedNodes
|
||||
}
|
||||
|
||||
processedNodes.push(node); // Add node for standard appending
|
||||
|
||||
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
|
||||
console.log(`[${this.instanceId}] Processing Node #${index + 1}: TextNode (non-empty)`);
|
||||
processedNodes.push(node); // Add text node
|
||||
} else {
|
||||
console.log(`[${this.instanceId}] Skipping Node #${index + 1} (Type: ${node.nodeType})`);
|
||||
}
|
||||
});
|
||||
|
||||
// --- 3. Append Processed Standard HTML & Source Nodes ---
|
||||
console.log(`[${this.instanceId}] Appending ${processedNodes.length} processed nodes to targetContainer (Context: ${renderContext})`);
|
||||
processedNodes.forEach(node => {
|
||||
targetContainer.appendChild(document.importNode(node, true));
|
||||
});
|
||||
console.log(`[${this.instanceId}] Finished appending nodes. Current targetContainer innerHTML length: ${targetContainer.innerHTML.length}`);
|
||||
|
||||
|
||||
// --- 4. Update State for Media Viewers ---
|
||||
console.log(`[${this.instanceId}] Evaluating media state (Context: ${renderContext}). mediaPlaceholderNeeded=${mediaPlaceholderNeeded}`);
|
||||
if (mediaPlaceholderNeeded) {
|
||||
// Images
|
||||
// Update Image Album State
|
||||
if (imagesForMedia.length > 0) {
|
||||
this.albumImages = [...imagesForMedia];
|
||||
this.showAlbumViewer = true;
|
||||
console.log(`[${this.instanceId}] SETTING Image Album state: show=${this.showAlbumViewer}, count=${this.albumImages.length}`);
|
||||
console.log(`[${this.instanceId}] SETTING Image Album state: show=true, count=${imagesForMedia.length}`);
|
||||
this.albumImages = [...imagesForMedia]; // Update with new images
|
||||
if (!this.showAlbumViewer) this.showAlbumViewer = true; // Trigger v-if
|
||||
} else {
|
||||
this.albumImages = [];
|
||||
this.showAlbumViewer = false;
|
||||
if (this.showAlbumViewer) {
|
||||
console.log(`[${this.instanceId}] RESETTING Image Album state: show=false`);
|
||||
this.showAlbumViewer = false; // Hide viewer if no images
|
||||
}
|
||||
if (this.albumImages.length > 0) this.albumImages = []; // Clear array
|
||||
}
|
||||
// Videos
|
||||
|
||||
// Update Video Album State
|
||||
if (videosForMedia.length > 0) {
|
||||
console.log(`[${this.instanceId}] SETTING Video Album state: show=true, count=${videosForMedia.length}`);
|
||||
this.albumVideos = [...videosForMedia];
|
||||
this.showVideoViewer = true;
|
||||
console.log(`[${this.instanceId}] SETTING Video Album state: show=${this.showVideoViewer}, count=${this.albumVideos.length}`);
|
||||
if (!this.showVideoViewer) this.showVideoViewer = true;
|
||||
} else {
|
||||
this.albumVideos = [];
|
||||
this.showVideoViewer = false;
|
||||
if (this.showVideoViewer) {
|
||||
console.log(`[${this.instanceId}] RESETTING Video Album state: show=false`);
|
||||
this.showVideoViewer = false;
|
||||
}
|
||||
if (this.albumVideos.length > 0) this.albumVideos = [];
|
||||
}
|
||||
// Audios
|
||||
|
||||
// Update Audio Album State
|
||||
if (audiosForMedia.length > 0) {
|
||||
console.log(`[${this.instanceId}] SETTING Audio Album state: show=true, count=${audiosForMedia.length}`);
|
||||
this.albumAudios = [...audiosForMedia];
|
||||
this.showAudioViewer = true;
|
||||
console.log(`[${this.instanceId}] SETTING Audio Album state: show=${this.showAudioViewer}, count=${this.albumAudios.length}`);
|
||||
if (!this.showAudioViewer) this.showAudioViewer = true;
|
||||
} else {
|
||||
this.albumAudios = [];
|
||||
this.showAudioViewer = false;
|
||||
if (this.showAudioViewer) {
|
||||
console.log(`[${this.instanceId}] RESETTING Audio Album state: show=false`);
|
||||
this.showAudioViewer = false;
|
||||
}
|
||||
if (this.albumAudios.length > 0) this.albumAudios = [];
|
||||
}
|
||||
} else {
|
||||
console.log(`[${this.instanceId}] No media elements found. Resetting all media states.`);
|
||||
this.resetMediaState(`no media found (${renderContext})`);
|
||||
|
||||
// If no media elements were found at all in this UI update
|
||||
if (!mediaPlaceholderNeeded && (this.showAlbumViewer || this.showVideoViewer || this.showAudioViewer)) {
|
||||
console.log(`[${this.instanceId}] No media elements found in this UI update. Resetting all media states.`);
|
||||
this.resetMediaState(`no media found in UI (Render #${renderContext})`);
|
||||
}
|
||||
|
||||
// --- 5. Handle Original Scripts (Still generally discouraged for security/complexity) ---
|
||||
// const scripts = doc.body.getElementsByTagName('script');
|
||||
// console.log(`[${this.instanceId}] Found ${scripts.length} script tags in parsed body (Execution is disabled).`);
|
||||
// ... logic to handle scripts if ever needed ...
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${this.instanceId}] Error during DOM parsing or processing (Context: Render #${renderContext}):`, error);
|
||||
// Consider resetting state or showing an error message in the UI
|
||||
this.cleanupDynamicContent(`error during render #${renderContext}`); // Attempt cleanup on error
|
||||
}
|
||||
|
||||
// --- 5. Handle Original Scripts (Still discouraged) ---
|
||||
// const scripts = doc.body.getElementsByTagName('script');
|
||||
// ...
|
||||
|
||||
console.log(`[${this.instanceId}] --- Finished renderContent (Context: ${renderContext}) ---`);
|
||||
console.log(`%c[${this.instanceId}] --- Finished renderContent (Context: Render #${renderContext}) ---`, 'color: blue; font-weight: bold;');
|
||||
},
|
||||
|
||||
injectScopedCss(css, renderContext) {
|
||||
console.log(`[${this.instanceId}] Injecting scoped CSS (Context: ${renderContext})`);
|
||||
injectScopedCss(css, context) {
|
||||
console.log(`[${this.instanceId}] Attempting to inject scoped CSS (Context: ${context})`);
|
||||
if (!css || !css.trim()) {
|
||||
console.warn(`[${this.instanceId}] CSS content is empty, skipping injection.`);
|
||||
console.warn(`[${this.instanceId}] CSS content is empty or whitespace-only, skipping injection (Context: ${context}).`);
|
||||
return;
|
||||
}
|
||||
const scopedCss = this.scopeCSS(css);
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = scopedCss;
|
||||
document.head.appendChild(styleElement);
|
||||
this.injectedStyleElements.push(styleElement);
|
||||
console.log(`[${this.instanceId}] Injected style element. Total injected: ${this.injectedStyleElements.length}`);
|
||||
try {
|
||||
const scopedCss = this.scopeCSS(css);
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = scopedCss;
|
||||
// Append to document head so it applies globally but selectors are scoped
|
||||
document.head.appendChild(styleElement);
|
||||
this.injectedStyleElements.push(styleElement); // Track for removal
|
||||
console.log(`[${this.instanceId}] Injected style element #${this.injectedStyleElements.length} into <head> (Context: ${context}).`);
|
||||
} catch (error) {
|
||||
console.error(`[${this.instanceId}] Error injecting scoped CSS (Context: ${context}):`, error);
|
||||
}
|
||||
},
|
||||
|
||||
scopeCSS(css) {
|
||||
// Ensure the container ID is available for scoping selectors
|
||||
const id = this.containerId;
|
||||
if (!id) {
|
||||
console.error(`[${this.instanceId}] Cannot scope CSS: containerId is missing!`);
|
||||
if (!id) {
|
||||
console.error(`[${this.instanceId}] Cannot scope CSS: containerId is missing or empty! Returning original CSS.`);
|
||||
return css;
|
||||
}
|
||||
const idSelector = `#${id}`;
|
||||
// Regex needs careful adjustment for custom elements like <internet_source>
|
||||
// This simpler prefixing approach is generally safer for dynamic content.
|
||||
return css.replace(/([^\r\n,{}\s][^{}]*)(?=\s*\{)/g, (match, selector) => {
|
||||
selector = selector.trim();
|
||||
if (selector.startsWith('@') || selector.startsWith('%') || /^\d+%$/.test(selector) || selector.includes(idSelector)) {
|
||||
return selector;
|
||||
}
|
||||
const scopedSelector = selector.split(',')
|
||||
.map(part => `${idSelector} ${part.trim()}`)
|
||||
.join(', ');
|
||||
return scopedSelector;
|
||||
}
|
||||
const idSelector = `#${id}`; // e.g., #dynamic-ui-instance123
|
||||
|
||||
// Improved Regex to handle various selectors including direct children (>), adjacent siblings (+), general siblings (~)
|
||||
// and pseudo-classes/elements (:hover, ::before, etc.)
|
||||
// It avoids scoping @-rules (like @keyframes, @media), pseudo-selectors starting with ':', and root selectors like 'html', 'body'.
|
||||
// It tries to prepend the ID selector correctly.
|
||||
const scopedCss = css.replace(/(^|[\s,}\]])([^@:%\s>+~][^{>,+~]*?)(\s*\{)/g, (match, prefix, selector, suffix) => {
|
||||
const trimmedSelector = selector.trim();
|
||||
|
||||
// Skip scoping for already scoped selectors, @ rules, percentages, pseudo-classes/elements, html/body
|
||||
if (trimmedSelector.startsWith(idSelector) ||
|
||||
trimmedSelector.startsWith('@') ||
|
||||
trimmedSelector.startsWith(':') || // Handles pseudo-classes/elements
|
||||
trimmedSelector.startsWith('%') ||
|
||||
/^\d+%$/.test(trimmedSelector) ||
|
||||
trimmedSelector === 'html' ||
|
||||
trimmedSelector === 'body') {
|
||||
// console.log(`[${this.instanceId}] scopeCSS: Skipping selector "${trimmedSelector}"`);
|
||||
return match; // Return the original match
|
||||
}
|
||||
|
||||
// Prepend the ID selector carefully
|
||||
const scopedSelector = trimmedSelector.split(',')
|
||||
.map(part => {
|
||||
part = part.trim();
|
||||
// Prepend ID selector, handling potential combinators at the start
|
||||
if (part.startsWith('>') || part.startsWith('+') || part.startsWith('~')) {
|
||||
return `${idSelector}${part}`; // e.g., #container>div
|
||||
} else {
|
||||
return `${idSelector} ${part}`; // e.g., #container div
|
||||
}
|
||||
})
|
||||
.join(', ');
|
||||
// console.log(`[${this.instanceId}] scopeCSS: Scoping "${trimmedSelector}" to "${scopedSelector}"`);
|
||||
return `${prefix}${scopedSelector}${suffix}`;
|
||||
});
|
||||
// console.log(`[${this.instanceId}] scopeCSS: Final scoped CSS:\n${scopedCss}`);
|
||||
return scopedCss;
|
||||
},
|
||||
|
||||
handleContainerClick(event) {
|
||||
const target = event.target;
|
||||
console.log(`[${this.instanceId}] Container clicked. Target: <${target.tagName}>, Classes: ${target.classList}`);
|
||||
// Log the exact element clicked
|
||||
console.log(`[${this.instanceId}] Container clicked. Target: <${target.tagName.toLowerCase()}>`, target);
|
||||
|
||||
// Use closest() to find the relevant interactive element, even if the click was on a child
|
||||
const clickablePost = target.closest('img.clickable-post');
|
||||
if (clickablePost && clickablePost.dataset.endpoint) {
|
||||
event.preventDefault();
|
||||
if (clickablePost) {
|
||||
console.log(`[${this.instanceId}] Click detected on or within 'img.clickable-post'.`);
|
||||
const endpoint = clickablePost.dataset.endpoint;
|
||||
const payloadKey = clickablePost.dataset.payloadKey || 'img_path';
|
||||
const payloadKey = clickablePost.dataset.payloadKey; // Already defaulted during render
|
||||
const src = clickablePost.getAttribute('src');
|
||||
|
||||
if (!endpoint) {
|
||||
console.error(`[${this.instanceId}] Clickable POST image is missing 'data-endpoint'! Cannot send POST.`);
|
||||
return;
|
||||
}
|
||||
if (!src) {
|
||||
console.error(`[${this.instanceId}] Clickable POST image is missing 'src'! Cannot determine payload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault(); // Prevent any default image behavior if applicable
|
||||
const payload = { [payloadKey]: src };
|
||||
console.log(`[${this.instanceId}] Clickable POST triggered. Endpoint: ${endpoint}, Payload:`, payload);
|
||||
console.log(`[${this.instanceId}] Triggering POST request. Endpoint: ${endpoint}, Payload:`, payload);
|
||||
|
||||
axios.post(endpoint, payload)
|
||||
.then(response => console.log(`[${this.instanceId}] Post to ${endpoint} successful:`, response.data))
|
||||
.catch(error => console.error(`[${this.instanceId}] Error posting to ${endpoint}:`, error));
|
||||
return;
|
||||
.then(response => console.log(`[${this.instanceId}] POST to ${endpoint} successful:`, response.data))
|
||||
.catch(error => console.error(`[${this.instanceId}] Error posting to ${endpoint}:`, error.response ? error.response.data : error.message));
|
||||
return; // Handled
|
||||
}
|
||||
|
||||
const openFolderLink = target.closest('a.open-folder');
|
||||
if (openFolderLink && openFolderLink.dataset.discussionId) {
|
||||
event.preventDefault();
|
||||
if (openFolderLink) {
|
||||
console.log(`[${this.instanceId}] Click detected on or within 'a.open-folder'.`);
|
||||
event.preventDefault(); // Prevent default link behavior (already set href="#")
|
||||
const discussionId = openFolderLink.dataset.discussionId;
|
||||
console.log(`[${this.instanceId}] Open folder link clicked. Discussion ID: ${discussionId}`);
|
||||
|
||||
if (!discussionId) {
|
||||
console.error(`[${this.instanceId}] Open folder link is missing 'data-discussion-id'! Cannot open folder.`);
|
||||
return;
|
||||
}
|
||||
if (!this.clientId) {
|
||||
console.error(`[${this.instanceId}] ERROR: Client ID not found in Vuex store for open_discussion_folder!`);
|
||||
alert("Error: Client information is missing.");
|
||||
console.error(`[${this.instanceId}] CRITICAL: Client ID not found in Vuex store! Cannot send open_discussion_folder request.`);
|
||||
alert("Error: Client information is missing. Cannot perform this action.");
|
||||
return;
|
||||
}
|
||||
console.log(`[${this.instanceId}] Posting to /open_discussion_folder with client_id: ${this.clientId}, discussion_id: ${discussionId}`);
|
||||
axios.post('/open_discussion_folder', { client_id: this.clientId, discussion_id: discussionId })
|
||||
|
||||
const payload = { client_id: this.clientId, discussion_id: discussionId };
|
||||
console.log(`[${this.instanceId}] Posting to '/open_discussion_folder'. Payload:`, payload);
|
||||
|
||||
axios.post('/open_discussion_folder', payload)
|
||||
.then(response => console.log(`[${this.instanceId}] Open folder request successful:`, response.data))
|
||||
.catch(error => console.error(`[${this.instanceId}] Error opening folder:`, error));
|
||||
return;
|
||||
.catch(error => console.error(`[${this.instanceId}] Error requesting open folder:`, error.response ? error.response.data : error.message));
|
||||
return; // Handled
|
||||
}
|
||||
|
||||
// Handle clicks on <internet_source> to open href
|
||||
const internetSource = target.closest('internet_source');
|
||||
if (internetSource && internetSource.hasAttribute('href')) {
|
||||
event.preventDefault();
|
||||
// Handle clicks on <internet_source> to open its href
|
||||
const internetSource = target.closest('internet_source[href]'); // Target only those with href
|
||||
if (internetSource) {
|
||||
event.preventDefault(); // Prevent any potential default behavior
|
||||
const href = internetSource.getAttribute('href');
|
||||
console.log(`[${this.instanceId}] Internet Source clicked. Opening href: ${href}`);
|
||||
window.open(href, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
console.log(`[${this.instanceId}] Internet Source clicked. Opening href in new tab: ${href}`);
|
||||
window.open(href, '_blank', 'noopener,noreferrer'); // Security best practice
|
||||
return; // Handled
|
||||
}
|
||||
|
||||
// Potentially handle clicks on <local_source> differently if needed (e.g., show details)
|
||||
// Handle clicks on <local_source> (currently just logs info)
|
||||
const localSource = target.closest('local_source');
|
||||
if (localSource) {
|
||||
event.preventDefault(); // Prevent any default action
|
||||
const summary = localSource.getAttribute('summary') || 'No summary provided.';
|
||||
const similarity = localSource.getAttribute('similarity');
|
||||
console.log(`[${this.instanceId}] Local Source clicked. Summary: ${summary}, Similarity: ${similarity}%`);
|
||||
// Could potentially trigger a modal or other UI element here
|
||||
if (localSource) {
|
||||
event.preventDefault(); // Prevent any potential default behavior
|
||||
const summary = localSource.getAttribute('summary') || 'No summary available.';
|
||||
const similarity = localSource.getAttribute('similarity') || 'N/A';
|
||||
console.log(`[${this.instanceId}] Local Source clicked. Summary: "${summary}", Similarity: ${similarity}%`);
|
||||
// Optional: Implement further UI interaction like showing details in a modal
|
||||
// alert(`Local Source:\nSimilarity: ${similarity}%\nSummary: ${summary}`);
|
||||
return;
|
||||
return; // Handled
|
||||
}
|
||||
|
||||
|
||||
console.log(`[${this.instanceId}] Click was not handled by specific handlers.`);
|
||||
// If the click wasn't on any known interactive element within the dynamic content
|
||||
console.log(`[${this.instanceId}] Click occurred but was not on a recognized interactive element (clickable-post, open-folder, internet_source[href], local_source). No action taken.`);
|
||||
},
|
||||
|
||||
cleanupDynamicContent(cleanupContext) {
|
||||
console.log(`[${this.instanceId}] cleanupDynamicContent CALLED (Context: ${cleanupContext})`);
|
||||
// Provides context on *why* cleanup is happening (e.g., before new render, on unmount)
|
||||
console.log(`%c[${this.instanceId}] cleanupDynamicContent CALLED (Context: ${cleanupContext})`, 'color: orange;');
|
||||
|
||||
// Reset reactive data for all media viewers
|
||||
// 1. Reset reactive data for all media viewers to ensure they are removed/reset
|
||||
this.resetMediaState(`cleanup (${cleanupContext})`);
|
||||
|
||||
// Remove injected stylesheets
|
||||
console.log(`[${this.instanceId}] Removing ${this.injectedStyleElements.length} injected style elements.`);
|
||||
// 2. Remove injected stylesheets from the document <head>
|
||||
console.log(`[${this.instanceId}] Removing ${this.injectedStyleElements.length} injected style elements (Context: ${cleanupContext}).`);
|
||||
this.injectedStyleElements.forEach((styleElement, index) => {
|
||||
// Check if the element still exists and has a parent before trying to remove
|
||||
if (styleElement?.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
// console.log(`[${this.instanceId}] Removed style element #${index + 1}.`);
|
||||
} else {
|
||||
console.warn(`[${this.instanceId}] Could not remove style element #${index + 1}.`);
|
||||
// Warn if removal fails, might indicate issues elsewhere or double-cleanup
|
||||
console.warn(`[${this.instanceId}] Could not remove style element #${index + 1}. It might have already been removed or was never properly appended.`);
|
||||
}
|
||||
});
|
||||
this.injectedStyleElements = [];
|
||||
this.injectedStyleElements = []; // Clear the tracking array
|
||||
|
||||
// Clear dynamically injected HTML content
|
||||
// 3. Clear dynamically injected HTML content from its container
|
||||
const htmlContainer = this.$refs.htmlContentContainer;
|
||||
if (htmlContainer) {
|
||||
console.log(`[${this.instanceId}] Clearing innerHTML of htmlContentContainer.`);
|
||||
console.log(`[${this.instanceId}] Clearing innerHTML of htmlContentContainer (Context: ${cleanupContext}).`);
|
||||
htmlContainer.innerHTML = '';
|
||||
} else {
|
||||
console.warn(`[${this.instanceId}] htmlContentContainer ref not found during cleanup (Context: ${cleanupContext}).`);
|
||||
// This might happen during unmount if refs are already gone, or if render failed initially
|
||||
console.warn(`[${this.instanceId}] htmlContentContainer ref not found during cleanup (Context: ${cleanupContext}). Cannot clear innerHTML.`);
|
||||
}
|
||||
console.log(`[${this.instanceId}] --- Finished cleanupDynamicContent (Context: ${cleanupContext}) ---`);
|
||||
console.log(`%c[${this.instanceId}] --- Finished cleanupDynamicContent (Context: ${cleanupContext}) ---`, 'color: orange;');
|
||||
},
|
||||
|
||||
resetMediaState(context) {
|
||||
// Centralized method to reset all media-related state, with logging
|
||||
console.log(`[${this.instanceId}] Resetting media state (Context: ${context})`);
|
||||
let changed = false;
|
||||
if (this.showAlbumViewer) { this.showAlbumViewer = false; changed = true; }
|
||||
if (this.albumImages.length > 0) { this.albumImages = []; changed = true; }
|
||||
if (this.showVideoViewer) { this.showVideoViewer = false; changed = true; }
|
||||
if (this.albumVideos.length > 0) { this.albumVideos = []; changed = true; }
|
||||
if (this.showAudioViewer) { this.showAudioViewer = false; changed = true; }
|
||||
if (this.albumAudios.length > 0) { this.albumAudios = []; changed = true; }
|
||||
if (!changed) {
|
||||
console.log(`[${this.instanceId}] Media state was already reset.`);
|
||||
let stateChanged = false;
|
||||
|
||||
if (this.showAlbumViewer) {
|
||||
this.showAlbumViewer = false;
|
||||
console.log(`[${this.instanceId}] - Set showAlbumViewer = false`);
|
||||
stateChanged = true;
|
||||
}
|
||||
if (this.albumImages.length > 0) {
|
||||
this.albumImages = [];
|
||||
console.log(`[${this.instanceId}] - Cleared albumImages array`);
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
if (this.showVideoViewer) {
|
||||
this.showVideoViewer = false;
|
||||
console.log(`[${this.instanceId}] - Set showVideoViewer = false`);
|
||||
stateChanged = true;
|
||||
}
|
||||
if (this.albumVideos.length > 0) {
|
||||
this.albumVideos = [];
|
||||
console.log(`[${this.instanceId}] - Cleared albumVideos array`);
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
if (this.showAudioViewer) {
|
||||
this.showAudioViewer = false;
|
||||
console.log(`[${this.instanceId}] - Set showAudioViewer = false`);
|
||||
stateChanged = true;
|
||||
}
|
||||
if (this.albumAudios.length > 0) {
|
||||
this.albumAudios = [];
|
||||
console.log(`[${this.instanceId}] - Cleared albumAudios array`);
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
if (!stateChanged) {
|
||||
console.log(`[${this.instanceId}] - Media state was already in its default (reset) state.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -449,55 +589,74 @@ export default {
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4; /* Improve readability */
|
||||
}
|
||||
|
||||
:deep(internet_source) {
|
||||
border-left: 3px solid #007bff; /* Blue accent for internet */
|
||||
}
|
||||
/* Style internet sources with href to look clickable */
|
||||
:deep(internet_source[href]) {
|
||||
cursor: pointer; /* Indicate clickability */
|
||||
color: #0056b3; /* Link-like color */
|
||||
}
|
||||
:deep(internet_source[href]:hover) {
|
||||
background-color: #eef; /* Slight hover effect */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:deep(local_source) {
|
||||
border-left: 3px solid #28a745; /* Green accent for local */
|
||||
}
|
||||
|
||||
/* Optional: Style for icons within sources if provided */
|
||||
:deep(internet_source[icon]),
|
||||
:deep(local_source[icon]) {
|
||||
/* Basic icon styling idea - adjust as needed */
|
||||
/* This assumes the icon attr contains a URL */
|
||||
/* You might prefer using ::before with content: attr(icon) and background-image */
|
||||
padding-left: 28px; /* Make space for icon */
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
background-size: 16px 16px; /* Adjust size */
|
||||
background-image: var(--icon-url); /* Set via JS or more complex CSS if needed */
|
||||
/* A simple implementation might just rely on the backend sending an <img> inside */
|
||||
/* Style local sources to indicate they might be interactable */
|
||||
:deep(local_source) {
|
||||
cursor: default; /* Or 'pointer' if you add interaction */
|
||||
}
|
||||
:deep(local_source:hover) {
|
||||
background-color: #efe; /* Slight hover effect for consistency */
|
||||
}
|
||||
|
||||
/* Example if you want to display attributes directly (less common) */
|
||||
/*
|
||||
:deep(local_source)::after {
|
||||
content: ' (Similarity: ' attr(similarity) '%)';
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
margin-left: 5px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Style for icons within sources if the backend sends an <img> tag inside */
|
||||
:deep(internet_source img),
|
||||
:deep(local_source img) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle; /* Align icon nicely with text */
|
||||
}
|
||||
|
||||
/* Display the summary attribute's content */
|
||||
/* Using ::before allows text content and potential icon coexist */
|
||||
:deep(internet_source::before),
|
||||
:deep(local_source::before) {
|
||||
content: attr(summary); /* Display summary text */
|
||||
/* Additional styling for summary if needed */
|
||||
/* Add styles if needed, e.g., font-weight */
|
||||
/* display: inline-block; Optional: for better control with icons */
|
||||
/* vertical-align: middle; */
|
||||
}
|
||||
|
||||
/* Add similarity info after local source content */
|
||||
:deep(local_source)::after {
|
||||
content: ' (Similarity: ' attr(similarity) '%)';
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
margin-left: 8px;
|
||||
/* display: inline-block; */
|
||||
/* vertical-align: middle; */
|
||||
}
|
||||
|
||||
|
||||
/* Ensure container has some height */
|
||||
:deep(#dynamic-ui-root) {
|
||||
min-height: 20px;
|
||||
/* Ensure the main container has some visual space */
|
||||
div[id^="dynamic-ui-"] {
|
||||
min-height: 10px; /* Small minimum height */
|
||||
padding: 5px; /* Some internal padding */
|
||||
border: 1px dashed transparent; /* Invisible border for layout debugging if needed */
|
||||
}
|
||||
|
||||
/* Style for the debug output */
|
||||
pre {
|
||||
white-space: pre-wrap; /* Allow wrapping */
|
||||
word-break: break-all; /* Break long strings */
|
||||
}
|
||||
</style>
|
@ -92,8 +92,8 @@ import axios from 'axios';
|
||||
export default {
|
||||
props: {
|
||||
personality: {
|
||||
type: Object,
|
||||
required: true
|
||||
type: [Object, null],
|
||||
default: null
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
|
233
web/src/main.js
233
web/src/main.js
@ -8,6 +8,9 @@ import './assets/tailwind.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const STARRED_LOCAL_STORAGE_KEY = 'lollms_starred_personalities';
|
||||
const STARRED_FUNCTIONS_LOCAL_STORAGE_KEY = 'lollms_starred_functions';
|
||||
const STARRED_DISCUSSIONS_LOCAL_STORAGE_KEY = 'lollms_starred_discussions';
|
||||
|
||||
function copyObject(obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
@ -31,43 +34,24 @@ function copyObject(obj) {
|
||||
return objCopy;
|
||||
}
|
||||
|
||||
function loadStarredFromLocalStorage() {
|
||||
function loadStarredFromLocalStorage(key, description) {
|
||||
try {
|
||||
const stored = localStorage.getItem(STARRED_LOCAL_STORAGE_KEY);
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
console.error("Failed to load starred personalities from localStorage:", e);
|
||||
console.error(`Failed to load starred ${description} from localStorage:`, e);
|
||||
saveStarredToLocalStorage(key,[], description);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveStarredToLocalStorage(starredPaths) {
|
||||
function saveStarredToLocalStorage(key, starredItems, description) {
|
||||
try {
|
||||
localStorage.setItem(STARRED_LOCAL_STORAGE_KEY, JSON.stringify(starredPaths));
|
||||
localStorage.setItem(key, JSON.stringify(starredItems));
|
||||
} catch (e) {
|
||||
console.error("Failed to save starred personalities to localStorage:", e);
|
||||
console.error(`Failed to save starred ${description} to localStorage:`, e);
|
||||
}
|
||||
}
|
||||
const STARRED_FUNCTIONS_LOCAL_STORAGE_KEY = 'lollms_starred_functions';
|
||||
|
||||
function loadStarredFunctionsFromLocalStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STARRED_FUNCTIONS_LOCAL_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (e) {
|
||||
console.error("Failed to load starred functions from localStorage:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveStarredFunctionsToLocalStorage(starredPaths) {
|
||||
try {
|
||||
localStorage.setItem(STARRED_FUNCTIONS_LOCAL_STORAGE_KEY, JSON.stringify(starredPaths));
|
||||
} catch (e) {
|
||||
console.error("Failed to save starred functions to localStorage:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const store = createStore({
|
||||
state () {
|
||||
@ -97,8 +81,8 @@ export const store = createStore({
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
client_id:"",
|
||||
leftPanelCollapsed: false,
|
||||
rightPanelCollapsed: true,
|
||||
leftPanelCollapsed: localStorage.getItem('lollms_webui_left_panel_collapsed') === 'true',
|
||||
rightPanelCollapsed: localStorage.getItem('lollms_webui_right_panel_collapsed') !== 'false', // Default to true if not set or false
|
||||
view_mode: localStorage.getItem('lollms_webui_view_mode') || 'compact',
|
||||
yesNoDialog:null,
|
||||
universalForm:null,
|
||||
@ -112,6 +96,7 @@ export const store = createStore({
|
||||
ready:false,
|
||||
loading_infos: "",
|
||||
loading_progress: 0,
|
||||
progress_value:0,
|
||||
version : "unknown",
|
||||
settingsChanged:false,
|
||||
isConnected: false,
|
||||
@ -124,8 +109,10 @@ export const store = createStore({
|
||||
modelsArr:[],
|
||||
selectedModel:null,
|
||||
personalities:[],
|
||||
starredPersonalities: loadStarredFromLocalStorage(), // Initialize from localStorage
|
||||
starredFunctions: loadStarredFunctionsFromLocalStorage(), // NEW: starred functions state
|
||||
functions:[], // Added state for functions if managed globally
|
||||
starredPersonalities: loadStarredFromLocalStorage(STARRED_LOCAL_STORAGE_KEY, 'personalities'),
|
||||
starredFunctions: loadStarredFromLocalStorage(STARRED_FUNCTIONS_LOCAL_STORAGE_KEY, 'functions'),
|
||||
starredDiscussions: loadStarredFromLocalStorage(STARRED_DISCUSSIONS_LOCAL_STORAGE_KEY, 'discussions'),
|
||||
diskUsage:null,
|
||||
ramUsage:null,
|
||||
vramUsage:null,
|
||||
@ -163,12 +150,15 @@ export const store = createStore({
|
||||
state.view_mode = mode;
|
||||
localStorage.setItem('lollms_webui_view_mode', mode);
|
||||
},
|
||||
setFunctions(state, functions) { state.functions = functions; }, // NEW: if managing functions in store
|
||||
|
||||
setStarredFunctions(state, starredPaths) { // NEW
|
||||
setFunctions(state, functions) { state.functions = functions; },
|
||||
setStarredFunctions(state, starredPaths) {
|
||||
state.starredFunctions = starredPaths;
|
||||
saveStarredFunctionsToLocalStorage(starredPaths);
|
||||
},
|
||||
saveStarredToLocalStorage(STARRED_FUNCTIONS_LOCAL_STORAGE_KEY, starredPaths, 'functions');
|
||||
},
|
||||
setStarredDiscussions(state, starredIds) {
|
||||
state.starredDiscussions = starredIds;
|
||||
saveStarredToLocalStorage(STARRED_DISCUSSIONS_LOCAL_STORAGE_KEY, starredIds, 'discussions');
|
||||
},
|
||||
setYesNoDialog(state, dialog) { state.yesNoDialog = dialog; },
|
||||
setUniversalForm(state, form) { state.universalForm = form; },
|
||||
setSaveConfiguration(state, saveFn) { state.saveConfiguration = saveFn; },
|
||||
@ -181,6 +171,7 @@ export const store = createStore({
|
||||
setIsReady(state, ready) { state.ready = ready; },
|
||||
setLoadingInfos(state, infos) { state.loading_infos = infos; },
|
||||
setLoadingProgress(state, progress) { state.loading_progress = progress; },
|
||||
setProgressValue(state, progress_value) { state.progress_value = progress_value; },
|
||||
setVersion(state, version) { state.version = version; },
|
||||
setSettingsChanged(state, changed) { state.settingsChanged = changed; },
|
||||
setIsConnected(state, isConnected) { state.isConnected = isConnected; },
|
||||
@ -193,28 +184,47 @@ export const store = createStore({
|
||||
setModelsArr(state, modelsArr) { state.modelsArr = modelsArr; },
|
||||
setSelectedModel(state, selectedModel) { state.selectedModel = selectedModel; },
|
||||
setPersonalities(state, personalities) { state.personalities = personalities; },
|
||||
setStarredPersonalities(state, starredPaths) { state.starredPersonalities = starredPaths; saveStarredToLocalStorage(starredPaths); },
|
||||
setStarredPersonalities(state, starredPaths) {
|
||||
state.starredPersonalities = starredPaths;
|
||||
saveStarredToLocalStorage(STARRED_LOCAL_STORAGE_KEY, starredPaths, 'personalities');
|
||||
},
|
||||
addStarredPersonality(state, personalityPath) {
|
||||
try{
|
||||
if (!state.starredPersonalities.includes(personalityPath)) {
|
||||
state.starredPersonalities.push(personalityPath);
|
||||
saveStarredToLocalStorage(state.starredPersonalities);
|
||||
}
|
||||
|
||||
}catch{console.log("error")}
|
||||
if (!state.starredPersonalities.includes(personalityPath)) {
|
||||
state.starredPersonalities.push(personalityPath);
|
||||
saveStarredToLocalStorage(STARRED_LOCAL_STORAGE_KEY, state.starredPersonalities, 'personalities');
|
||||
}
|
||||
},
|
||||
removeStarredPersonality(state, personalityPath) {
|
||||
const index = state.starredPersonalities.indexOf(personalityPath);
|
||||
if (index > -1) {
|
||||
state.starredPersonalities.splice(index, 1);
|
||||
saveStarredToLocalStorage(state.starredPersonalities);
|
||||
saveStarredToLocalStorage(STARRED_LOCAL_STORAGE_KEY, state.starredPersonalities, 'personalities');
|
||||
}
|
||||
},
|
||||
removeStarredDiscussion(state, personalityPath) {
|
||||
const index = state.starredPersonalities.indexOf(personalityPath);
|
||||
addStarredFunction(state, functionPath) {
|
||||
if (!state.starredFunctions.includes(functionPath)) {
|
||||
state.starredFunctions.push(functionPath);
|
||||
saveStarredToLocalStorage(STARRED_FUNCTIONS_LOCAL_STORAGE_KEY, state.starredFunctions, 'functions');
|
||||
}
|
||||
},
|
||||
removeStarredFunction(state, functionPath) {
|
||||
const index = state.starredFunctions.indexOf(functionPath);
|
||||
if (index > -1) {
|
||||
state.starredPersonalities.splice(index, 1);
|
||||
saveStarredToLocalStorage(state.starredPersonalities);
|
||||
state.starredFunctions.splice(index, 1);
|
||||
saveStarredToLocalStorage(STARRED_FUNCTIONS_LOCAL_STORAGE_KEY, state.starredFunctions, 'functions');
|
||||
}
|
||||
},
|
||||
addStarredDiscussion(state, discussionId) {
|
||||
if (!state.starredDiscussions.includes(discussionId)) {
|
||||
state.starredDiscussions.push(discussionId);
|
||||
saveStarredToLocalStorage(STARRED_DISCUSSIONS_LOCAL_STORAGE_KEY, state.starredDiscussions, 'discussions');
|
||||
}
|
||||
},
|
||||
removeStarredDiscussion(state, discussionId) {
|
||||
const index = state.starredDiscussions.indexOf(discussionId);
|
||||
if (index > -1) {
|
||||
state.starredDiscussions.splice(index, 1);
|
||||
saveStarredToLocalStorage(STARRED_DISCUSSIONS_LOCAL_STORAGE_KEY, state.starredDiscussions, 'discussions');
|
||||
}
|
||||
},
|
||||
setDiskUsage(state, diskUsage) { state.diskUsage = diskUsage; },
|
||||
@ -226,21 +236,7 @@ export const store = createStore({
|
||||
setCurrentModel(state, currentModel) { state.currentModel = currentModel; },
|
||||
setCurrentBinding(state, currentBinding){ state.currentBinding = currentBinding; },
|
||||
setDatabases(state, databases) { state.databases = databases; },
|
||||
addStarredFunction(state, functionPath) { // NEW
|
||||
if (!state.starredFunctions.includes(functionPath)) {
|
||||
state.starredFunctions.push(functionPath);
|
||||
saveStarredFunctionsToLocalStorage(state.starredFunctions);
|
||||
}
|
||||
},
|
||||
removeStarredFunction(state, functionPath) { // NEW
|
||||
const index = state.starredFunctions.indexOf(functionPath);
|
||||
if (index > -1) {
|
||||
state.starredFunctions.splice(index, 1);
|
||||
saveStarredFunctionsToLocalStorage(state.starredFunctions);
|
||||
}
|
||||
},
|
||||
|
||||
updateFunction(state, newFunctionData){ // NEW: If managing functions in store
|
||||
updateFunction(state, newFunctionData){
|
||||
const index = state.functions.findIndex(f => (f.id || f.full_path) === (newFunctionData.id || newFunctionData.full_path));
|
||||
if (index !== -1) {
|
||||
const updatedFunc = { ...state.functions[index], ...newFunctionData };
|
||||
@ -248,7 +244,6 @@ export const store = createStore({
|
||||
} else {
|
||||
console.warn("Couldn't update function (not found):", newFunctionData.full_path);
|
||||
}
|
||||
// Maybe update mounted list if needed? For functions, this might not be necessary unless they are tied to config
|
||||
},
|
||||
updatePersonality(state, newPersonality){
|
||||
const index = state.personalities.findIndex(p => (p.id || p.full_path) === (newPersonality.id || newPersonality.full_path));
|
||||
@ -303,6 +298,7 @@ export const store = createStore({
|
||||
getIsReady: state => state.ready,
|
||||
getLoadingInfos: state => state.loading_infos,
|
||||
getLoadingProgress: state => state.loading_progress,
|
||||
getProgressValue: state => state.progress_value,
|
||||
getVersion: state => state.version,
|
||||
getSettingsChanged: state => state.settingsChanged,
|
||||
getIsConnected: state => state.isConnected,
|
||||
@ -325,8 +321,9 @@ export const store = createStore({
|
||||
getCurrentModel: state => state.currentModel,
|
||||
getCurrentBinding: state => state.currentBinding,
|
||||
getDatabases: state => state.databases,
|
||||
getFunctions: state => state.functions, // NEW: if managing functions in store
|
||||
getStarredFunctions: state => state.starredFunctions, // NEW
|
||||
getFunctions: state => state.functions,
|
||||
getStarredFunctions: state => state.starredFunctions,
|
||||
getStarredDiscussions: state => state.starredDiscussions,
|
||||
},
|
||||
actions: {
|
||||
async fetchIsRtOn({ commit }) {
|
||||
@ -357,8 +354,7 @@ export const store = createStore({
|
||||
commit('setVersion', 'unknown (error)');
|
||||
}
|
||||
},
|
||||
// NEW: Action to toggle function star status
|
||||
toggleStarFunction({ commit, state, dispatch }, func) {
|
||||
toggleStarFunction({ commit, state }, func) {
|
||||
if (!func || !func.full_path) {
|
||||
console.warn("Attempted to toggle star on invalid function:", func);
|
||||
return;
|
||||
@ -371,10 +367,37 @@ export const store = createStore({
|
||||
} else {
|
||||
commit('addStarredFunction', functionPath);
|
||||
}
|
||||
// Update the isStarred status in the main functions list if managed by store
|
||||
// If function list is local to component, component needs to handle its own state update
|
||||
// dispatch('updateFunctionStarredStatus', { functionPath, isStarred: !isCurrentlyStarred }); // Uncomment if functions list is in store
|
||||
},
|
||||
toggleStarPersonality({ commit, state, dispatch }, personality) {
|
||||
if (!personality || !personality.full_path) {
|
||||
console.warn("Attempted to toggle star on invalid personality:", personality);
|
||||
return;
|
||||
}
|
||||
const personalityPath = personality.full_path;
|
||||
const isCurrentlyStarred = state.starredPersonalities.includes(personalityPath);
|
||||
|
||||
if (isCurrentlyStarred) {
|
||||
commit('removeStarredPersonality', personalityPath);
|
||||
} else {
|
||||
commit('addStarredPersonality', personalityPath);
|
||||
}
|
||||
dispatch('updatePersonalityStarredStatus', { personalityPath, isStarred: !isCurrentlyStarred });
|
||||
},
|
||||
toggleStarDiscussion({ commit, state }, discussion) {
|
||||
if (!discussion || typeof discussion.id === 'undefined') {
|
||||
console.warn("Attempted to toggle star on invalid discussion:", discussion);
|
||||
return;
|
||||
}
|
||||
const discussionId = discussion.id;
|
||||
const isCurrentlyStarred = state.starredDiscussions.includes(discussionId);
|
||||
|
||||
if (isCurrentlyStarred) {
|
||||
commit('removeStarredDiscussion', discussionId);
|
||||
} else {
|
||||
commit('addStarredDiscussion', discussionId);
|
||||
}
|
||||
// No global discussion list update action needed unless discussions are managed in Vuex state
|
||||
},
|
||||
async refreshConfig({ commit, state }) {
|
||||
console.log("Fetching configuration");
|
||||
try {
|
||||
@ -383,7 +406,6 @@ export const store = createStore({
|
||||
if (!configData) {
|
||||
throw new Error("Received null or undefined config file");
|
||||
}
|
||||
// Make a deep copy to avoid modifying the original response object if needed elsewhere
|
||||
let configFile = copyObject(configData);
|
||||
|
||||
if (!configFile.personalities || configFile.personalities.length === 0) {
|
||||
@ -412,7 +434,7 @@ export const store = createStore({
|
||||
commit('setSettingsChanged', false);
|
||||
} catch (error) {
|
||||
console.error('Error during refreshConfig:', error);
|
||||
commit('setSettingsChanged', false); // Assume config is stale, revert changes
|
||||
commit('setSettingsChanged', false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -483,7 +505,7 @@ export const store = createStore({
|
||||
let personalities = [];
|
||||
const catdictionary = await api_get_req("get_all_personalities");
|
||||
const mountedSet = new Set(state.config?.personalities || []);
|
||||
const starredSet = new Set(state.starredPersonalities || []); // Use starred from state
|
||||
const starredSet = new Set(state.starredPersonalities || []);
|
||||
|
||||
if (!catdictionary) {
|
||||
commit('setPersonalities', []);
|
||||
@ -502,7 +524,7 @@ export const store = createStore({
|
||||
const full_path = `${catkey}/${item.folder}`;
|
||||
const langPaths = Array.isArray(item.languages) ? item.languages.map(lang => `${full_path}:${lang}`) : [];
|
||||
const isMounted = mountedSet.has(full_path) || langPaths.some(lp => mountedSet.has(lp));
|
||||
const isStarred = starredSet.has(full_path); // Check against store state
|
||||
const isStarred = starredSet.has(full_path);
|
||||
|
||||
return {
|
||||
...item,
|
||||
@ -510,7 +532,7 @@ export const store = createStore({
|
||||
full_path: full_path,
|
||||
id: item.id || full_path,
|
||||
isMounted: isMounted,
|
||||
isStarred: isStarred, // Set initial starred status
|
||||
isStarred: isStarred,
|
||||
isProcessing: false,
|
||||
};
|
||||
}).filter(item => item !== null);
|
||||
@ -549,7 +571,7 @@ export const store = createStore({
|
||||
let pers = copyObject(personalitiesMap.get(basePath));
|
||||
pers.language = typeof full_path_item === 'string' && full_path_item.includes(':') ? full_path_item.split(':')[1] : '';
|
||||
pers.isMounted = true;
|
||||
pers.isStarred = starredSet.has(basePath); // Ensure mounted also gets starred status
|
||||
pers.isStarred = starredSet.has(basePath);
|
||||
mountedPersArr.push(pers);
|
||||
} else {
|
||||
indicesToRemove.push(i);
|
||||
@ -638,9 +660,6 @@ export const store = createStore({
|
||||
async refreshModels({ commit, state }) {
|
||||
try {
|
||||
let modelsArr = await api_get_req("list_models");
|
||||
console.log(`modelsArr: ${modelsArr}`)
|
||||
console.log(`state.modelsZoo:`)
|
||||
console.log(state.modelsZoo)
|
||||
if (!Array.isArray(modelsArr)) modelsArr = [];
|
||||
commit('setModelsArr', modelsArr);
|
||||
|
||||
@ -651,8 +670,6 @@ export const store = createStore({
|
||||
const modelsZoo = state.modelsZoo || [];
|
||||
const modelsArrSet = new Set(modelsArr);
|
||||
modelsZoo.forEach(item => {
|
||||
console.log("modelsArrSet")
|
||||
console.log(modelsArrSet)
|
||||
if (item && item.name) item.isInstalled = modelsArrSet.has(item.name);
|
||||
});
|
||||
commit('setModelsZoo', [...modelsZoo]);
|
||||
@ -736,39 +753,6 @@ export const store = createStore({
|
||||
}
|
||||
},
|
||||
|
||||
toggleStarPersonality({ commit, state, dispatch }, personality) {
|
||||
if (!personality || !personality.full_path) {
|
||||
console.warn("Attempted to toggle star on invalid personality:", personality);
|
||||
return;
|
||||
}
|
||||
const personalityPath = personality.full_path;
|
||||
const isCurrentlyStarred = state.starredPersonalities.includes(personalityPath);
|
||||
|
||||
if (isCurrentlyStarred) {
|
||||
commit('removeStarredPersonality', personalityPath);
|
||||
} else {
|
||||
commit('addStarredPersonality', personalityPath);
|
||||
}
|
||||
// Update the isStarred status in the main personalities list for immediate UI feedback
|
||||
dispatch('updatePersonalityStarredStatus', { personalityPath, isStarred: !isCurrentlyStarred });
|
||||
},
|
||||
toggleStarDiscussion({ commit, state, dispatch }, discussion) {
|
||||
if (!discussion) {
|
||||
console.warn("Attempted to toggle star on invalid discussion:", discussion);
|
||||
return;
|
||||
}
|
||||
const discussion_id = discussion.id;
|
||||
const isCurrentlyStarred = state.starredDiscussions.includes(discussion_id);
|
||||
|
||||
if (isCurrentlyStarred) {
|
||||
commit('removeStarredDiscussion', discussion_id);
|
||||
} else {
|
||||
commit('addStarredDiscussion', discussion_id);
|
||||
}
|
||||
// Update the isStarred status in the main personalities list for immediate UI feedback
|
||||
dispatch('updateDiscussionStarredStatus', { discussion_id, isStarred: !isCurrentlyStarred });
|
||||
},
|
||||
|
||||
updatePersonalityStarredStatus({ commit, state }, { personalityPath, isStarred }) {
|
||||
const personality = state.personalities.find(p => p.full_path === personalityPath);
|
||||
if (personality) {
|
||||
@ -777,13 +761,16 @@ export const store = createStore({
|
||||
console.warn("Could not find personality in main list to update starred status:", personalityPath);
|
||||
}
|
||||
},
|
||||
|
||||
async applyConfiguration({ commit, state }) {
|
||||
|
||||
async applyConfiguration({ commit, state, dispatch }) {
|
||||
try {
|
||||
const res = await axios.post('/apply_settings', { client_id:state.client_id, config: state.config }, { headers: state.posts_headers });
|
||||
if (res.data.status) {
|
||||
state.toast.showToast("Settings applied. Refreshing...", 4, true);
|
||||
await this.refreshConfigInView(); // Refreshes store, resets local state
|
||||
// Assuming refreshConfigInView exists in the component context where this action is called
|
||||
// If not, need to call refreshConfig directly or handle UI refresh differently
|
||||
// await this.refreshConfigInView(); // This line might cause issues if 'this' is not the component instance
|
||||
await dispatch('refreshConfig'); // Refresh store config state
|
||||
} else {
|
||||
state.toast.showToast(`Apply failed: ${res.data.error || 'Error'}`, 4, false);
|
||||
}
|
||||
@ -791,15 +778,15 @@ export const store = createStore({
|
||||
state.toast.showToast(`Error applying settings: ${error.message || error}`, 4, false);
|
||||
}
|
||||
},
|
||||
async saveConfiguration({ commit, state }) {
|
||||
this.isLoading = true;
|
||||
this.loading_text = "Saving configuration...";
|
||||
async saveConfiguration({ state }) {
|
||||
// Typically isLoading and loading_text are component-local state, not Vuex state
|
||||
// We'll assume the component calling this action handles its own loading state
|
||||
try {
|
||||
const res = await axios.post('/save_settings', { client_id:state.client_id }, { headers: state.posts_headers });
|
||||
if (res.data.status)state.toast.showToast("Settings saved successfully.", 4, true);
|
||||
if (res.data.status) state.toast.showToast("Settings saved successfully.", 4, true);
|
||||
else state.messageBox.showMessage(`Error saving settings: ${res.data.error || 'Error'}`);
|
||||
} catch (error) {state.messageBox.showMessage(`Error saving settings: ${error.message}`);
|
||||
} finally { this.isLoading = false; }
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div v-if="isReady" class="flex flex-row h-screen w-screen overflow-hidden">
|
||||
<LeftPanel
|
||||
:show-left-panel="showLeftPanel"
|
||||
:discussions-list="discussionsList"
|
||||
:discussions-list="discussionsListWithStarred"
|
||||
:current-discussion="currentDiscussion"
|
||||
:toolbar-loading="isGenerating"
|
||||
:formatted-database-name="formatted_database_name"
|
||||
@ -43,7 +43,7 @@
|
||||
@export-discussions-as-markdown="exportDiscussionsAsMarkdown"
|
||||
@show-database-selector="showDatabaseSelector"
|
||||
@import-discussion-file="importDiscussionFile"
|
||||
@toggle-star-discussion="toggleStarDiscussion"
|
||||
@toggle-star-discussion="handleToggleStarDiscussion"
|
||||
/>
|
||||
|
||||
<ChatArea
|
||||
@ -129,7 +129,7 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
discussionsList: [],
|
||||
discussionsList: [], // Base list fetched from API
|
||||
currentDiscussion: {},
|
||||
discussionArr: [],
|
||||
personalityAvatars: [],
|
||||
@ -188,9 +188,14 @@ export default defineComponent({
|
||||
'ready', 'loading_infos', 'loading_progress', 'version', 'config',
|
||||
'databases', 'isConnected', 'isGenerating', 'client_id', 'leftPanelCollapsed',
|
||||
'rightPanelCollapsed', 'theme_vars', 'selectedPersonality',
|
||||
'currentPersonConfig', 'personalities', 'personalities_ready'
|
||||
'currentPersonConfig', 'personalities', 'personalities_ready',
|
||||
'starredDiscussions' // Make sure this is in mapState or mapGetters
|
||||
]),
|
||||
...mapGetters([
|
||||
'getIsReady', 'getVersion', 'getConfig', 'getClientId', 'getDatabases',
|
||||
'getIsConnected', 'getIsGenerating', 'getLeftPanelCollapsed',
|
||||
'getRightPanelCollapsed', 'getStarredDiscussions' // Use getter if defined
|
||||
]),
|
||||
...mapGetters(['getIsReady', 'getVersion', 'getConfig', 'getClientId', 'getDatabases', 'getIsConnected', 'getIsGenerating', 'getLeftPanelCollapsed', 'getRightPanelCollapsed']),
|
||||
isReady() {
|
||||
return this.getIsReady;
|
||||
},
|
||||
@ -208,6 +213,14 @@ export default defineComponent({
|
||||
const db_name = this.config?.discussion_db_name || "default";
|
||||
return db_name.replace(/_/g, " ");
|
||||
},
|
||||
// Computed property to add 'isStarred' to the discussions list for the LeftPanel prop
|
||||
discussionsListWithStarred() {
|
||||
const starredSet = new Set(this.getStarredDiscussions); // Use getter or direct state
|
||||
return this.discussionsList.map(discussion => ({
|
||||
...discussion,
|
||||
isStarred: starredSet.has(discussion.id)
|
||||
}));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['refreshConfig', 'refreshDatabase', 'refreshBindings', 'refreshPersonalitiesZoo', 'refreshMountedPersonalities', 'refreshModelsZoo', 'refreshModels', 'fetchLanguages', 'fetchLanguage', 'fetchIsRtOn', 'toggleStarPersonality', 'toggleStarDiscussion', 'applyConfiguration', 'saveConfiguration', 'refreshModelStatus']),
|
||||
@ -264,10 +277,11 @@ export default defineComponent({
|
||||
this.loading = true;
|
||||
const res = await axios.get('/list_discussions');
|
||||
if (res && Array.isArray(res.data)) {
|
||||
// Store the raw list, the computed prop will add 'isStarred'
|
||||
this.discussionsList = res.data.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
created_at: item.created_at, // Keep creation time for sorting/grouping
|
||||
created_at: item.created_at,
|
||||
loading: false,
|
||||
})).sort((a, b) => b.id - a.id);
|
||||
} else { this.discussionsList = []; }
|
||||
@ -280,9 +294,10 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
loadLastUsedDiscussion() {
|
||||
const id = localStorage.getItem('selected_discussion');
|
||||
if (id) {
|
||||
const discussionItem = this.discussionsList.find(d => String(d.id) === id);
|
||||
const id_str = localStorage.getItem('selected_discussion');
|
||||
if (id_str) {
|
||||
const id = parseInt(id_str, 10); // Ensure ID is a number for comparison
|
||||
const discussionItem = this.discussionsList.find(d => d.id === id);
|
||||
if (discussionItem) { this.selectDiscussion(discussionItem); }
|
||||
else { localStorage.removeItem('selected_discussion'); this.currentDiscussion = {}; this.discussionArr = []; }
|
||||
} else { this.currentDiscussion = {}; this.discussionArr = []; }
|
||||
@ -291,7 +306,8 @@ export default defineComponent({
|
||||
selectDiscussion(item) {
|
||||
if (this.isGenerating) { this.$store.state.toast.showToast("Please wait for generation to finish or stop.", 4, false); return; }
|
||||
if (item && this.currentDiscussion?.id !== item.id) {
|
||||
this.currentDiscussion = { ...item }; this.setPageTitle(item);
|
||||
this.currentDiscussion = { ...item }; // Copy item, don't need isStarred here
|
||||
this.setPageTitle(item);
|
||||
localStorage.setItem('selected_discussion', item.id); this.load_discussion(item.id);
|
||||
} else if (!item) {
|
||||
this.currentDiscussion = {}; this.discussionArr = []; this.setPageTitle(); localStorage.removeItem('selected_discussion');
|
||||
@ -332,6 +348,10 @@ export default defineComponent({
|
||||
await axios.post('/delete_discussion', { client_id: this.client_id, id: id });
|
||||
this.$store.state.toast.showToast(`Discussion ${id} deleted.`, 4, true);
|
||||
this.discussionsList = this.discussionsList.filter(d => d.id !== id);
|
||||
// Remove from starred if it was starred
|
||||
if (this.getStarredDiscussions.includes(id)) {
|
||||
this.$store.commit('removeStarredDiscussion', id);
|
||||
}
|
||||
if (this.currentDiscussion?.id === id) { this.selectDiscussion(null); }
|
||||
} catch (error) {
|
||||
console.error("Error deleting discussion:", error); this.$store.state.toast.showToast(`Error deleting discussion ${id}: ${error.message}`, 4, false); this.setDiscussionLoading(id, false);
|
||||
@ -349,10 +369,16 @@ export default defineComponent({
|
||||
this.$store.state.toast.showToast(`Deleting ${numToDelete} discussions...`, 5, true);
|
||||
let deletedCount = 0; let failedCount = 0;
|
||||
idsToDelete.forEach(id => this.setDiscussionLoading(id, true));
|
||||
const currentStarred = this.getStarredDiscussions; // Get starred before loop
|
||||
for (const id of idsToDelete) {
|
||||
try {
|
||||
await axios.post('/delete_discussion', { client_id: this.client_id, id: id });
|
||||
deletedCount++; this.discussionsList = this.discussionsList.filter(d => d.id !== id);
|
||||
deletedCount++;
|
||||
this.discussionsList = this.discussionsList.filter(d => d.id !== id);
|
||||
// Remove from starred if it was starred
|
||||
if (currentStarred.includes(id)) {
|
||||
this.$store.commit('removeStarredDiscussion', id);
|
||||
}
|
||||
if (this.currentDiscussion?.id === id) { this.selectDiscussion(null); }
|
||||
} catch (error) { console.error(`Error deleting discussion ${id}:`, error); failedCount++; this.setDiscussionLoading(id, false); }
|
||||
}
|
||||
@ -397,9 +423,15 @@ export default defineComponent({
|
||||
catch (error) { console.error("Error opening folder:", error); this.$store.state.toast.showToast(`Could not open folder: ${error.message}`, 4, false); }
|
||||
},
|
||||
|
||||
toggleStarDiscussion(item) {
|
||||
this.toggleStarDiscussion(item.id);
|
||||
this.$nextTick(() => { this.$forceUpdate(); }); // May be needed for LeftPanel list re-render based on getter
|
||||
// Renamed handler to avoid conflict with mapActions name
|
||||
handleToggleStarDiscussion(item) {
|
||||
if (item && item.id !== undefined) {
|
||||
// Dispatch the action from the store
|
||||
this.toggleStarDiscussion(item); // Pass the whole item as the action expects {id: ...}
|
||||
} else {
|
||||
console.warn("Attempted to toggle star on invalid discussion item:", item);
|
||||
}
|
||||
// No need for $forceUpdate, Vuex reactivity + computed prop should handle it
|
||||
},
|
||||
|
||||
load_discussion(id, callback) {
|
||||
@ -493,7 +525,7 @@ export default defineComponent({
|
||||
if (this.isGenerating) { this.$store.state.toast.showToast("Please wait for the current response.", 4, false); return; }
|
||||
|
||||
this.$store.commit('setIsGenerating', true); this.setDiscussionLoading(this.currentDiscussion.id, true);
|
||||
const emitEvent = type === 'internet' ? 'generate_msg_with_internet' : 'generate_msg';
|
||||
const emitEvent = type === 'internet' ? 'generate_msg_with_internet' : 'generate_msg';
|
||||
socket.emit(emitEvent, { prompt: message });
|
||||
this.scrollToBottomMessages();
|
||||
},
|
||||
@ -557,8 +589,13 @@ export default defineComponent({
|
||||
createEmptyAIMessage() { if (!this.currentDiscussion?.id) return; socket.emit('create_empty_message', { type: 1 }); this.$store.state.toast.showToast("Creating empty AI message...", 2, true); },
|
||||
|
||||
setDiscussionLoading(id, isLoading) {
|
||||
const index = this.discussionsList.findIndex(d => d.id === id);
|
||||
if(id!==undefined){
|
||||
const index = this.discussionsList.findIndex(d => d.id === id);
|
||||
if (index !== -1) { this.discussionsList[index].loading = isLoading; }
|
||||
}
|
||||
else{
|
||||
console.error("discussion id is undefined")
|
||||
}
|
||||
},
|
||||
setPageTitle(item = null) {
|
||||
const baseTitle = 'L🌟LLMS WebUI'; let discussionTitle = "Welcome";
|
||||
@ -570,7 +607,7 @@ export default defineComponent({
|
||||
},
|
||||
scrollToDiscussionElement(id) {
|
||||
if (!id) return;
|
||||
nextTick(() => { const el = document.getElementById(`dis-${id}`); const container = document.getElementById('leftPanelScroll'); if (el && container) { container.scrollTo({ top: el.offsetTop, behavior: 'smooth' }); } });
|
||||
nextTick(() => { const el = document.getElementById(id); const container = document.getElementById('leftPanelScroll'); if (el && container) { container.scrollTo({ top: el.offsetTop, behavior: 'smooth' }); } });
|
||||
},
|
||||
copyToClipBoard(messageEntry) {
|
||||
let content = messageEntry.message.content || ""; let result = content;
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<transition name="slide-right">
|
||||
<div v-if="showLeftPanel" class="relative flex flex-col no-scrollbar shadow-lg w-[16rem] panels-color scrollbar h-full">
|
||||
<!-- Header -->
|
||||
<RouterLink :to="{ name: 'discussions' }" class="flex items-center space-x-2 p-2 border-b border-blue-200 dark:border-blue-700 hover:bg-blue-50 dark:hover:bg-blue-800 transition duration-150 ease-in-out">
|
||||
<div class="logo-container w-12 h-12 flex-shrink-0">
|
||||
<img class="w-full h-full rounded-full object-cover logo-image border-2 border-blue-300 dark:border-blue-600 shadow-sm"
|
||||
@ -21,6 +22,7 @@
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Toolbar
|
||||
:loading="toolbarLoading"
|
||||
:is-checkbox="isCheckbox"
|
||||
@ -45,6 +47,7 @@
|
||||
@show-personality-list="$emit('show-personality-list')"
|
||||
/>
|
||||
|
||||
<!-- Search & Sort -->
|
||||
<div class="w-full max-w-md mx-auto p-2 border-b border-blue-100 dark:border-blue-800">
|
||||
<form @submit.prevent class="relative">
|
||||
<div class="flex items-center space-x-1">
|
||||
@ -72,11 +75,13 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions (Checkbox Mode) -->
|
||||
<div v-if="isCheckbox" class="w-full p-2 bg-blue-100 dark:bg-blue-900 border-b border-blue-200 dark:border-blue-700">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<p v-if="selectedDiscussions.length > 0" class="text-sm text-blue-700 dark:text-blue-200">Selected: {{ selectedDiscussions.length }}</p>
|
||||
<div v-if="selectedDiscussions.length > 0" class="flex space-x-1 items-center">
|
||||
<button v-if="!showConfirmation" class="svg-button text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200" title="Remove selected" type="button" @click.stop="showConfirmation = true">
|
||||
<div class="flex space-x-1 items-center">
|
||||
<!-- Delete -->
|
||||
<button v-if="!showConfirmation && selectedDiscussions.length > 0" class="svg-button text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200" title="Remove selected" type="button" @click.stop="showConfirmation = true">
|
||||
<i data-feather="trash" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<div v-if="showConfirmation" class="flex space-x-1 items-center">
|
||||
@ -87,24 +92,29 @@
|
||||
<i data-feather="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1 items-center">
|
||||
<button class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 rotate-90" title="Export selected to a json file" type="button" @click.stop="$emit('export-discussions-as-json', selectedDiscussions)">
|
||||
<!-- Export -->
|
||||
<button v-if="selectedDiscussions.length > 0" class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 rotate-90" title="Export selected to a json file" type="button" @click.stop="$emit('export-discussions-as-json', selectedDiscussions)">
|
||||
<i data-feather="codepen" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<button class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 rotate-90" title="Export selected to a folder" type="button" @click.stop="$emit('export-discussions-to-folder', selectedDiscussions)">
|
||||
<button v-if="selectedDiscussions.length > 0" class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 rotate-90" title="Export selected to a folder" type="button" @click.stop="$emit('export-discussions-to-folder', selectedDiscussions)">
|
||||
<i data-feather="folder" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<button class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" title="Export selected to a markdown file" type="button" @click.stop="$emit('export-discussions-as-markdown', selectedDiscussions)">
|
||||
<button v-if="selectedDiscussions.length > 0" class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" title="Export selected to a markdown file" type="button" @click.stop="$emit('export-discussions-as-markdown', selectedDiscussions)">
|
||||
<i data-feather="bookmark" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<button class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" title="Select/Deselect All" type="button" @click.stop="selectAllDiscussions">
|
||||
<!-- Select All / Deselect All -->
|
||||
<button class="svg-button text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" :title="isAllSelected ? 'Deselect All Visible' : 'Select All Visible'" type="button" @click.stop="selectAllDiscussions">
|
||||
<i :data-feather="isAllSelected ? 'minus-square' : 'check-square'" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<!-- Star/Unstar Selected -->
|
||||
<button v-if="selectedDiscussions.length > 0" class="svg-button text-yellow-500 hover:text-yellow-700 dark:text-yellow-400 dark:hover:text-yellow-200" :title="selectedDiscussions.some(d=>d.isStarred) ? 'Unstar Selected':'Star Selected'" type="button" @click.stop="toggleStarSelectedDiscussions">
|
||||
<i :data-feather="selectedDiscussions.some(d=>d.isStarred) ? 'star' : 'star'" :fill="selectedDiscussions.some(d=>d.isStarred) ? 'currentColor' : 'none'" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discussions List -->
|
||||
<div id="leftPanelScroll" class="flex flex-col flex-grow overflow-y-auto overflow-x-hidden scrollbar"
|
||||
@dragover.prevent="isDragOverDiscussion = true" @dragleave="isDragOverDiscussion = false" @drop.prevent="handleDrop">
|
||||
<div class="relative flex flex-col flex-grow mb-10 z-0 w-full">
|
||||
@ -112,16 +122,17 @@
|
||||
<div id="dis-list" :class="(filterInProgress || toolbarLoading) ? 'opacity-20 pointer-events-none' : ''" class="flex flex-col flex-grow w-full pb-10">
|
||||
<TransitionGroup name="discussionsList">
|
||||
<template v-for="item in groupedDiscussions" :key="item.key">
|
||||
<!-- Group Header -->
|
||||
<div v-if="item.type === 'header'"
|
||||
class="sticky top-0 z-10 px-2 py-1 bg-gray-100 dark:bg-gray-800 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider shadow-sm flex items-center justify-between cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
@click="toggleSection(item.key)">
|
||||
<span>{{ item.label }}</span>
|
||||
<i :data-feather="item.collapsed ? 'chevron-right' : 'chevron-down'" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<!-- Inside LeftPanel.vue's TransitionGroup -->
|
||||
<!-- Discussion Item -->
|
||||
<Discussion
|
||||
v-if="item.type === 'discussion'"
|
||||
:id="`dis-${item.data.id}`"
|
||||
v-if="item.type === 'discussion' && !item.collapsed"
|
||||
:id="item.data.id"
|
||||
:title="item.data.title"
|
||||
:selected="currentDiscussion && currentDiscussion.id === item.data.id"
|
||||
:loading="item.data.loading"
|
||||
@ -151,6 +162,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex flex-row items-center justify-center border-t border-blue-200 dark:border-blue-700 p-1">
|
||||
<div class="chat-bar text-center flex items-center justify-center w-full cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-700 rounded transition duration-150 ease-in-out" @click="$emit('show-database-selector')">
|
||||
<button class="svg-button p-1">
|
||||
@ -168,7 +180,7 @@
|
||||
<script>
|
||||
import feather from 'feather-icons';
|
||||
import { nextTick } from 'vue';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { mapState } from 'vuex'; // Removed mapGetters
|
||||
import { RouterLink } from 'vue-router';
|
||||
import storeLogo from '@/assets/logo.png';
|
||||
import Discussion from './Discussion.vue';
|
||||
@ -194,7 +206,7 @@ export default {
|
||||
components: { Discussion, RouterLink, Toolbar },
|
||||
props: {
|
||||
showLeftPanel: Boolean,
|
||||
discussionsList: Array,
|
||||
discussionsList: Array, // Expects array with { id, title, created_at, loading, isStarred }
|
||||
currentDiscussion: Object,
|
||||
toolbarLoading: Boolean,
|
||||
formattedDatabaseName: String,
|
||||
@ -216,15 +228,14 @@ export default {
|
||||
showConfirmation: false,
|
||||
isDragOverDiscussion: false,
|
||||
searchTimeout: null,
|
||||
localDiscussionsState: [],
|
||||
localDiscussionsState: [], // Holds { id, checkBoxValue }
|
||||
sortBy: 'date',
|
||||
sortOrder: 'desc',
|
||||
collapsedSections: { starred: false, today: false, yesterday: true, older: true },
|
||||
collapsedSections: { starred: false, today: false, yesterday: true, older: true }, // Keep track of collapsed state for each group
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['config', 'theme_vars']),
|
||||
...mapGetters(['getStarredDiscussionsSet']),
|
||||
|
||||
logoSrc() {
|
||||
return this.config?.app_custom_logo ? `/user_infos/${this.config.app_custom_logo}` : storeLogo;
|
||||
@ -238,9 +249,6 @@ export default {
|
||||
appSlogan() {
|
||||
return this.config?.app_custom_slogan || 'One tool to rule them all';
|
||||
},
|
||||
starredSet() {
|
||||
return this.getStarredDiscussionsSet || new Set();
|
||||
},
|
||||
sortIcon() {
|
||||
return this.sortOrder === 'asc' ? 'arrow-up' : 'arrow-down';
|
||||
},
|
||||
@ -248,42 +256,43 @@ export default {
|
||||
const labels = { date: 'Date', title: 'Title' };
|
||||
return labels[this.sortBy] || 'Date';
|
||||
},
|
||||
// Enhance the prop list with local checkbox state and parsed date
|
||||
enhancedDiscussions() {
|
||||
return (this.discussionsList || []).map(disc => {
|
||||
const localState = this.localDiscussionsState.find(ld => ld.id === disc.id);
|
||||
const creationDate = disc.created_at ? new Date(disc.created_at) : new Date(0); // Use created_at
|
||||
const creationDate = disc.created_at ? new Date(disc.created_at) : new Date(0);
|
||||
return {
|
||||
...disc,
|
||||
...disc, // Includes id, title, created_at, loading, isStarred (from prop)
|
||||
checkBoxValue: localState ? localState.checkBoxValue : false,
|
||||
isStarred: this.starredSet.has(String(disc.id)),
|
||||
creationDate: creationDate,
|
||||
creationDate: creationDate, // Parsed date for sorting/grouping
|
||||
};
|
||||
});
|
||||
},
|
||||
filteredDiscussions() {
|
||||
// This is primarily used for 'Select All' logic now, filtering happens inside groupedDiscussions
|
||||
// Primarily used for 'Select All' logic and determining if all *visible* items are selected
|
||||
filteredDiscussions() {
|
||||
if (!this.filterTitle.trim()) {
|
||||
return this.enhancedDiscussions;
|
||||
}
|
||||
const query = this.filterTitle.toLowerCase();
|
||||
return this.enhancedDiscussions.filter(item => item.title && item.title.toLowerCase().includes(query));
|
||||
},
|
||||
// Groups discussions by Starred, Today, Yesterday, Older, applies filtering and sorting
|
||||
groupedDiscussions() {
|
||||
const starred = [];
|
||||
const today = [];
|
||||
const yesterday = [];
|
||||
const older = [];
|
||||
|
||||
// Filter first based on search query
|
||||
// Filter first based on search query using the enhanced list
|
||||
const filtered = this.enhancedDiscussions.filter(item => {
|
||||
if (!this.filterTitle.trim()) return true;
|
||||
const query = this.filterTitle.toLowerCase();
|
||||
return item.title && item.title.toLowerCase().includes(query);
|
||||
});
|
||||
|
||||
// Separate starred from unstarred
|
||||
// Separate into groups (Starred first, then by date)
|
||||
filtered.forEach(disc => {
|
||||
if (disc.isStarred) {
|
||||
if (disc.isStarred) { // Use isStarred from the enhanced list (which came from props)
|
||||
starred.push(disc);
|
||||
} else {
|
||||
const creationDate = disc.creationDate;
|
||||
@ -305,7 +314,6 @@ export default {
|
||||
} else if (this.sortBy === 'title') {
|
||||
comparison = (a.title || '').localeCompare(b.title || ''); // Ascending by title default
|
||||
}
|
||||
// Apply sort order direction
|
||||
const orderMultiplier = (this.sortBy === 'date' && this.sortOrder === 'asc') || (this.sortBy === 'title' && this.sortOrder === 'desc') ? -1 : 1;
|
||||
return comparison * orderMultiplier;
|
||||
};
|
||||
@ -316,51 +324,32 @@ export default {
|
||||
older.sort(sortFn);
|
||||
|
||||
const groups = [];
|
||||
|
||||
// Add Starred section
|
||||
if (starred.length > 0) {
|
||||
groups.push({ type: 'header', label: 'Starred', key: 'starred', collapsed: this.collapsedSections.starred });
|
||||
if (!this.collapsedSections.starred) {
|
||||
starred.forEach(item => groups.push({ type: 'discussion', data: item, key: `disc-${item.id}` }));
|
||||
}
|
||||
const addGroup = (label, key, items) => {
|
||||
if (items.length > 0) {
|
||||
const isCollapsed = this.collapsedSections[key] || false;
|
||||
groups.push({ type: 'header', label: label, key: key, collapsed: isCollapsed });
|
||||
items.forEach(item => groups.push({ type: 'discussion', data: item, key: `disc-${item.id}`, collapsed: isCollapsed }));
|
||||
}
|
||||
}
|
||||
|
||||
// Add Today section
|
||||
if (today.length > 0) {
|
||||
groups.push({ type: 'header', label: 'Today', key: 'today', collapsed: this.collapsedSections.today });
|
||||
if (!this.collapsedSections.today) {
|
||||
today.forEach(item => groups.push({ type: 'discussion', data: item, key: `disc-${item.id}` }));
|
||||
}
|
||||
}
|
||||
addGroup('Starred', 'starred', starred);
|
||||
addGroup('Today', 'today', today);
|
||||
addGroup('Yesterday', 'yesterday', yesterday);
|
||||
addGroup('Older', 'older', older);
|
||||
|
||||
// Add Yesterday section
|
||||
if (yesterday.length > 0) {
|
||||
groups.push({ type: 'header', label: 'Yesterday', key: 'yesterday', collapsed: this.collapsedSections.yesterday });
|
||||
if (!this.collapsedSections.yesterday) {
|
||||
yesterday.forEach(item => groups.push({ type: 'discussion', data: item, key: `disc-${item.id}` }));
|
||||
}
|
||||
}
|
||||
|
||||
// Add Older section
|
||||
if (older.length > 0) {
|
||||
groups.push({ type: 'header', label: 'Older', key: 'older', collapsed: this.collapsedSections.older });
|
||||
if (!this.collapsedSections.older) {
|
||||
older.forEach(item => groups.push({ type: 'discussion', data: item, key: `disc-${item.id}` }));
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
|
||||
// Determines which discussions are currently selected based on local checkbox state
|
||||
selectedDiscussions() {
|
||||
// Selected items are based on the local checkbox state, across all filtered items
|
||||
return this.filteredDiscussions.filter(item => {
|
||||
// We need to filter enhancedDiscussions based on local checkbox state
|
||||
return this.enhancedDiscussions.filter(item => {
|
||||
const localState = this.localDiscussionsState.find(ld => ld.id === item.id);
|
||||
return localState && localState.checkBoxValue;
|
||||
});
|
||||
},
|
||||
// Determines if all *visible* (filtered) discussions are currently selected
|
||||
isAllSelected() {
|
||||
// Check if all items matching the current filter are selected
|
||||
const targetList = this.filteredDiscussions;
|
||||
const targetList = this.filteredDiscussions; // Discussions matching the current filter
|
||||
if (targetList.length === 0) return false;
|
||||
const selectedIds = new Set(this.selectedDiscussions.map(d => d.id));
|
||||
return targetList.every(item => selectedIds.has(item.id));
|
||||
@ -370,7 +359,8 @@ export default {
|
||||
toggleSection(key) {
|
||||
if (key in this.collapsedSections) {
|
||||
this.collapsedSections[key] = !this.collapsedSections[key];
|
||||
// Recompute groupedDiscussions implicitly updates the view
|
||||
// Force reactivity update if needed, though computed should handle it
|
||||
// this.$forceUpdate(); // Avoid if possible
|
||||
this.$nextTick(() => feather.replace());
|
||||
}
|
||||
},
|
||||
@ -379,7 +369,6 @@ export default {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.filterInProgress = false;
|
||||
// Vue reactivity handles the update, feather needs refresh
|
||||
this.$nextTick(() => feather.replace());
|
||||
}, 300);
|
||||
},
|
||||
@ -397,26 +386,28 @@ export default {
|
||||
},
|
||||
deleteDiscussion(item) {
|
||||
this.localDiscussionsState = this.localDiscussionsState.filter(d => d.id !== item.id);
|
||||
this.$emit('delete-discussion', item.id);
|
||||
this.$emit('delete-discussion', item.id); // Emit ID to parent
|
||||
},
|
||||
checkUncheckDiscussion({ id, checked }) {
|
||||
const index = this.localDiscussionsState.findIndex(d => d.id === id);
|
||||
if (index !== -1) {
|
||||
this.localDiscussionsState[index].checkBoxValue = checked;
|
||||
} else {
|
||||
// Ensure item exists in discussionsList before adding to local state if needed
|
||||
// Check if the item actually exists in the main list before adding local state
|
||||
if (this.discussionsList.some(d => d.id === id)) {
|
||||
this.localDiscussionsState.push({ id: id, checkBoxValue: checked });
|
||||
} else {
|
||||
console.warn("Tried to check/uncheck an item not present in discussionsList:", id);
|
||||
}
|
||||
}
|
||||
},
|
||||
selectAllDiscussions() {
|
||||
const targetState = !this.isAllSelected;
|
||||
const filteredIds = new Set(this.filteredDiscussions.map(d => d.id)); // Use filtered list
|
||||
const filteredIds = new Set(this.filteredDiscussions.map(d => d.id)); // Get IDs of currently visible/filtered items
|
||||
|
||||
// Update local state for all items matching the filter
|
||||
// Update local state only for visible items
|
||||
this.enhancedDiscussions.forEach(item => {
|
||||
if (filteredIds.has(item.id)) {
|
||||
if (filteredIds.has(item.id)) { // Apply only to filtered items
|
||||
const index = this.localDiscussionsState.findIndex(d => d.id === item.id);
|
||||
if (index !== -1) {
|
||||
this.localDiscussionsState[index].checkBoxValue = targetState;
|
||||
@ -430,8 +421,9 @@ export default {
|
||||
},
|
||||
deleteSelectedDiscussions() {
|
||||
const idsToDelete = this.selectedDiscussions.map(d => d.id);
|
||||
// Clear local checkbox state for deleted items
|
||||
this.localDiscussionsState = this.localDiscussionsState.filter(ld => !idsToDelete.includes(ld.id));
|
||||
this.$emit('delete-selected', idsToDelete);
|
||||
this.$emit('delete-selected', idsToDelete); // Emit array of IDs
|
||||
this.showConfirmation = false;
|
||||
this.isCheckbox = false; // Exit checkbox mode after deletion
|
||||
},
|
||||
@ -441,33 +433,33 @@ export default {
|
||||
if (files.length === 1 && files[0].type === 'application/json') {
|
||||
this.$emit('import-discussion-file', files[0]);
|
||||
} else {
|
||||
// Consider using a more integrated notification system if available
|
||||
alert("Please drop a single JSON file to import.");
|
||||
// this.$store.dispatch('showToast', { message: "Please drop a single JSON file.", type: 'warning' });
|
||||
this.$store.state.toast.showToast("Please drop a single JSON file to import.", 4, false);
|
||||
}
|
||||
},
|
||||
toggleStarDiscussion(item) {
|
||||
this.$emit('toggle-star-discussion', item);
|
||||
// Assuming the parent/Vuex handles the actual state change and prop update
|
||||
this.$emit('toggle-star-discussion', item); // Emit the discussion item itself
|
||||
},
|
||||
toggleStarSelectedDiscussions() {
|
||||
const selected = this.selectedDiscussions;
|
||||
if (selected.length === 0) return;
|
||||
// Determine if we are starring or unstarring based on the first selected item's state
|
||||
const isStarring = selected.length > 0 ? !selected[0].isStarred : true; // Default to starring if unsure
|
||||
// Determine target state: if ANY selected are NOT starred, we STAR all. If ALL selected ARE starred, we UNSTAR all.
|
||||
const shouldStar = selected.some(item => !item.isStarred);
|
||||
|
||||
selected.forEach(item => {
|
||||
// Only emit toggle if the item's current state doesn't match the target state
|
||||
if (item.isStarred !== isStarring) {
|
||||
// Emit toggle only if the item's current state needs changing
|
||||
if (item.isStarred !== shouldStar) {
|
||||
this.toggleStarDiscussion(item);
|
||||
}
|
||||
});
|
||||
this.$nextTick(() => feather.replace());
|
||||
},
|
||||
// Syncs local checkbox state with the incoming discussions list prop
|
||||
syncLocalState(newList) {
|
||||
// Ensure localDiscussionsState reflects the current discussionsList, preserving checkbox values
|
||||
const currentIds = new Set((newList || []).map(d => d.id));
|
||||
const updatedLocalState = this.localDiscussionsState.filter(ld => currentIds.has(ld.id)); // Keep existing states for current items
|
||||
// Keep state for items that still exist, discard state for removed items
|
||||
const updatedLocalState = this.localDiscussionsState.filter(ld => currentIds.has(ld.id));
|
||||
// Add default (unchecked) state for new items
|
||||
(newList || []).forEach(disc => {
|
||||
// Add state for new items if they don't exist
|
||||
if (!updatedLocalState.some(ld => ld.id === disc.id)) {
|
||||
updatedLocalState.push({ id: disc.id, checkBoxValue: false });
|
||||
}
|
||||
@ -477,61 +469,43 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
discussionsList: {
|
||||
handler(newList, oldList) {
|
||||
// Check if lists differ significantly before syncing to avoid unnecessary updates
|
||||
if (JSON.stringify(newList) !== JSON.stringify(oldList)) {
|
||||
this.syncLocalState(newList);
|
||||
}
|
||||
// Always refresh icons after data changes that might affect layout/content
|
||||
this.$nextTick(() => feather.replace());
|
||||
handler(newList) {
|
||||
this.syncLocalState(newList); // Keep local checkbox state aligned
|
||||
this.$nextTick(() => feather.replace()); // Refresh icons after list changes
|
||||
},
|
||||
immediate: true,
|
||||
deep: true // Watch for changes within discussion objects if necessary (e.g., title changes externally)
|
||||
immediate: true, // Sync on initial load
|
||||
deep: false // Shallow watch is enough if parent guarantees object identity changes
|
||||
},
|
||||
isCheckbox(newVal) {
|
||||
this.$nextTick(() => feather.replace());
|
||||
if (!newVal) {
|
||||
this.showConfirmation = false;
|
||||
// Optionally clear local checkbox state when exiting checkbox mode
|
||||
// Optionally clear selections when exiting checkbox mode
|
||||
// this.localDiscussionsState.forEach(state => state.checkBoxValue = false);
|
||||
}
|
||||
},
|
||||
showConfirmation() {
|
||||
this.$nextTick(() => feather.replace());
|
||||
},
|
||||
// Watch groupedDiscussions might be too expensive, rely on reactivity + nextTick in methods
|
||||
// groupedDiscussions() {
|
||||
// this.$nextTick(() => feather.replace());
|
||||
// }
|
||||
filterTitle() {
|
||||
// No need for explicit feather call here, handleSearchInput does it after debounce
|
||||
},
|
||||
sortBy() {
|
||||
this.$nextTick(() => feather.replace());
|
||||
},
|
||||
sortOrder() {
|
||||
this.$nextTick(() => feather.replace());
|
||||
}
|
||||
// Watch computed properties only if necessary, often nextTick in methods is better
|
||||
// groupedDiscussions() { this.$nextTick(() => feather.replace()); },
|
||||
// filterTitle() { /* handled by handleSearchInput */ },
|
||||
// sortBy() { this.$nextTick(() => feather.replace()); },
|
||||
// sortOrder() { this.$nextTick(() => feather.replace()); }
|
||||
},
|
||||
mounted() {
|
||||
// Initial sync and icon replacement
|
||||
this.syncLocalState(this.discussionsList);
|
||||
this.syncLocalState(this.discussionsList); // Initial sync
|
||||
nextTick(() => {
|
||||
feather.replace();
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
// Ensure icons are replaced after any reactive update
|
||||
// Be cautious with this, might cause performance issues if updates are frequent.
|
||||
// Often better to call feather.replace() specifically after actions that change icons.
|
||||
// nextTick(() => {
|
||||
// feather.replace();
|
||||
// });
|
||||
// Generally avoid feather.replace() here unless absolutely necessary.
|
||||
// Prefer calling it specifically in methods/watchers after DOM changes.
|
||||
// nextTick(() => { feather.replace(); });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Transitions for list items */
|
||||
.discussionsList-move,
|
||||
.discussionsList-enter-active,
|
||||
.discussionsList-leave-active {
|
||||
@ -541,19 +515,16 @@ export default {
|
||||
.discussionsList-enter-from,
|
||||
.discussionsList-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
transform: translateX(-20px); /* Adjusted transform */
|
||||
}
|
||||
|
||||
/* Ensure leaving items don't disrupt layout */
|
||||
.discussionsList-leave-active {
|
||||
position: absolute;
|
||||
/* Adjust width based on actual panel width minus padding/margins if necessary */
|
||||
width: calc(16rem - 1rem); /* Example: panel width 16rem, padding x 0.5rem each side */
|
||||
/* Or use percentage if parent width is reliable */
|
||||
/* width: 100%; */
|
||||
width: calc(100% - 1rem); /* Adjust based on padding/margins inside the scroll container */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* Transition for the panel itself */
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition: transform 0.3s ease-out;
|
||||
@ -563,4 +534,10 @@ export default {
|
||||
.slide-right-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
/* Ensure sticky headers work well */
|
||||
.sticky {
|
||||
position: -webkit-sticky; /* For Safari */
|
||||
position: sticky;
|
||||
}
|
||||
</style>
|
@ -1 +1 @@
|
||||
Subproject commit bea9b3378c5d2dda95cead76de5b75ac3b96a192
|
||||
Subproject commit 35a483aff93632e7d060a14bdc2c4e67d0e245fb
|
@ -1 +1 @@
|
||||
Subproject commit 0f54e59d1557a8a63884175d6985b17dc9ea8659
|
||||
Subproject commit 0a38ab5a135dbac1f15832d44dd15753c57d8e68
|
Loading…
x
Reference in New Issue
Block a user