This commit is contained in:
Saifeddine ALOUI 2025-04-07 16:49:43 +02:00
parent 47ab5b57ca
commit 665014c81e
26 changed files with 179 additions and 4001 deletions

26
web/dist/assets/index-5mBsl5lX.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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-CTkhORQ_.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-BXY6uktx.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};

View File

@ -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-CTkhORQ_.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-BXY6uktx.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

File diff suppressed because one or more lines are too long

4
web/dist/index.html vendored
View File

@ -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-CTkhORQ_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DmcjkfZo.css">
<script type="module" crossorigin src="/assets/index-BXY6uktx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-5mBsl5lX.css">
</head>
<body>
<div id="app"></div>

View File

@ -187,7 +187,7 @@ import feather from 'feather-icons';
// Make sure this path is correct relative to your component file
import botImgPlaceholder from "../assets/logo.svg";
// Assuming DynamicUIRenderer is correctly imported and registered globally or locally
import DynamicUIRenderer from "@/components/DynamicUIRenderer.vue";
import DynamicUIRenderer from "@/components/MarkdownBundle/DynamicUIRenderer.vue";
// Ensure VITE_LOLLMS_API_BASEURL is available in your environment (.env file)
const bUrl = import.meta.env.VITE_LOLLMS_API_BASEURL || ''; // Provide a fallback

File diff suppressed because it is too large Load Diff

View File

@ -1,659 +0,0 @@
<template>
<div :id="`code-block-container-${message_id}`" class="code-block-container bg-bg-light-tone-panel dark:bg-bg-dark-tone-panel p-2 rounded-lg shadow-sm mb-4">
<!-- == Function Call Display == -->
<div v-if="isFunctionLanguage">
<div class="flex justify-between items-center px-2 py-1 mb-1 rounded-t-lg bg-gray-200 dark:bg-gray-700">
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">
<i data-feather="zap" class="w-3 h-3 inline-block mr-1 feather-small"></i> Function Call
</span>
<div class="flex flex-row space-x-1">
<button @click="executeCode" :title="executeTitle" class="code-block-button execute-button" :disabled="isExecuting || !isValidFunctionCall" aria-label="Execute Function Call">
<i :data-feather="executeIcon" :class="{'animate-spin': isExecuting}" class="w-4 h-4"></i>
</button>
<button @click="toggleFunctionDetails" :title="isFunctionDetailsVisible ? 'Hide Details' : 'Show Details'" class="code-block-button" aria-label="Toggle Function Details">
<i :data-feather="isFunctionDetailsVisible ? 'chevron-up' : 'chevron-down'" class="w-4 h-4"></i>
</button>
</div>
</div>
<div class="p-2 rounded-b-md bg-white dark:bg-gray-800">
<div class="flex items-center space-x-2 text-sm mb-1 cursor-pointer hover:opacity-80" @click="toggleFunctionDetails" role="button" :aria-expanded="isFunctionDetailsVisible">
<span class="font-semibold text-gray-700 dark:text-gray-300">Function:</span>
<span v-if="isValidFunctionCall" class="font-mono bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-gray-900 dark:text-gray-100 break-all">{{ functionName }}</span>
<span v-else class="flex items-center text-amber-600 dark:text-amber-400">
<i data-feather="alert-circle" class="w-4 h-4 mr-1 feather-small"></i> Invalid / Incomplete
</span>
</div>
<div v-show="isFunctionDetailsVisible" class="mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 max-h-60 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600">
<div v-if="isValidFunctionCall">
<h4 class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400 mb-2 sticky top-0 bg-white dark:bg-gray-800 py-1">Parameters:</h4>
<div v-if="hasParameters" class="space-y-2">
<div v-for="(value, key) in functionParametersObject" :key="key" class="parameter-item">
<div class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-0.5">{{ key }}:</div>
<div v-if="typeof value === 'object' && value !== null" class="text-xs font-mono bg-gray-100 dark:bg-gray-700 p-1 rounded text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">
{{ JSON.stringify(value, null, 2) }}
</div>
<div v-else-if="typeof value === 'boolean'" class="text-sm font-mono text-blue-600 dark:text-blue-400"> {{ String(value) }} </div>
<div v-else-if="typeof value === 'number'" class="text-sm font-mono text-green-700 dark:text-green-400"> {{ value }} </div>
<div v-else-if="value === null" class="text-sm font-mono text-purple-600 dark:text-purple-400">null</div>
<div v-else class="text-sm font-mono bg-gray-100 dark:bg-gray-700 p-1 rounded text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">"{{ String(value) }}"</div>
</div>
</div>
<span v-else class="text-xs text-gray-500 italic">No parameters provided.</span>
</div>
<div v-else>
<h4 class="text-xs font-semibold uppercase text-red-600 dark:text-red-400 mb-1">Invalid JSON Input:</h4>
<pre class="text-xs font-mono bg-red-50 dark:bg-red-900/20 p-2 rounded max-h-48 overflow-y-auto text-red-800 dark:text-red-300 whitespace-pre-wrap break-all">{{ safeCodeProp || '(empty)' }}</pre>
</div>
</div>
</div>
</div>
<!-- == Standard Code Block Display == -->
<div v-else>
<!-- Top Bar -->
<div class="flex justify-between items-center px-2 py-1 mb-1 rounded-t-lg bg-gray-200 dark:bg-gray-700">
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{ effectiveLanguageDisplay || 'plaintext' }}</span>
<div class="flex flex-row space-x-1 items-center">
<button v-if="!isEditing" @click="toggleEditMode" title="Edit Code" class="code-block-button" aria-label="Edit Code"><i data-feather="edit-2" class="w-4 h-4"></i></button>
<template v-if="isEditing">
<button @click="toggleEditMode" title="Finish Editing" class="code-block-button" aria-label="Finish Editing"><i data-feather="check" class="w-4 h-4"></i></button>
<button @click="undo" :disabled="!canUndo" title="Undo (Ctrl+Z)" class="code-block-button" aria-label="Undo Edit"><i data-feather="rotate-ccw" class="w-4 h-4"></i></button>
<button @click="redo" :disabled="!canRedo" title="Redo (Ctrl+Y)" class="code-block-button" aria-label="Redo Edit"><i data-feather="rotate-cw" class="w-4 h-4"></i></button>
<div class="h-4 w-px bg-gray-400 dark:bg-gray-600 mx-1"></div>
<button @click="toggleSearch" :title="isSearchVisible ? 'Hide Search' : 'Show Search'" class="code-block-button" :class="{'active-search-button': isSearchVisible}" aria-label="Toggle Search">
<i :data-feather="isSearchVisible ? 'x' : 'search'" class="w-4 h-4"></i>
</button>
</template>
<button @click="copyCode" :title="copyTitle" class="code-block-button" aria-label="Copy Code"><i :data-feather="copyIcon" class="w-4 h-4"></i></button>
<div v-if="!isEditing" class="h-4 w-px bg-gray-400 dark:bg-gray-600 mx-1"></div>
<template v-if="!isEditing">
<button v-if="canExecute" @click="executeCode" :title="executeTitle" class="code-block-button execute-button" :disabled="isExecuting" aria-label="Execute Code"><i :data-feather="executeIcon" :class="{'animate-spin': isExecuting}" class="w-4 h-4"></i></button>
<button v-if="canExecuteInNewTab" @click="executeCode_in_new_tab" :title="executeNewTabTitle" class="code-block-button execute-button" :disabled="isExecuting" aria-label="Execute Code in New Tab"><i :data-feather="executeNewTabIcon" :class="{'animate-spin': isExecuting}" class="w-4 h-4"></i></button>
<button @click="openFolder" title="Open Project Folder" class="code-block-button" aria-label="Open Project Folder"><i data-feather="folder" class="w-4 h-4"></i></button>
<button v-if="canOpenFolderInVsCode" @click="openFolderVsCode" title="Open Project Folder in VS Code" class="code-block-button" aria-label="Open Project Folder in VS Code"><img src="@/assets/vscode_black.svg" class="w-4 h-4 dark:hidden" alt="VS Code"><img src="@/assets/vscode.svg" class="w-4 h-4 hidden dark:inline" alt="VS Code"></button>
<button v-if="canOpenInVsCode" @click="openVsCode" title="Open Code in VS Code" class="code-block-button" aria-label="Open Code in VS Code"><img src="@/assets/vscode.svg" class="w-4 h-4" alt="VS Code"></button>
</template>
</div>
</div>
<!-- Search/Replace Panel -->
<div v-if="isEditing && isSearchVisible" class="search-replace-panel flex items-center space-x-2 p-2 bg-gray-100 dark:bg-gray-700 text-sm mb-1 rounded">
<input ref="searchInputRef" type="text" v-model.lazy="searchQuery" placeholder="Find" class="search-input flex-grow" aria-label="Search query" @keydown.enter.prevent="findNextAndHighlight" @keydown.shift.enter.prevent="findPreviousAndHighlight" />
<span class="search-status" aria-live="polite"> {{ searchStatusText }} </span>
<button @click="findPreviousAndHighlight" :disabled="!hasMatches" title="Previous Match (Shift+Enter)" class="code-block-button search-button" aria-label="Previous Match"><i data-feather="chevron-left" class="w-4 h-4"></i></button>
<button @click="findNextAndHighlight" :disabled="!hasMatches" title="Next Match (Enter)" class="code-block-button search-button" aria-label="Next Match"><i data-feather="chevron-right" class="w-4 h-4"></i></button>
<input type="text" v-model.lazy="replaceQuery" placeholder="Replace with" class="replace-input flex-grow" aria-label="Replace query" @keydown.enter.prevent="replaceCurrentAndFindNext" />
<button @click="replaceCurrent" :disabled="!hasActiveMatch" title="Replace Current" class="code-block-button search-button" aria-label="Replace Current">Replace</button>
<button @click="replaceAllMatches" :disabled="!hasMatches" title="Replace All" class="code-block-button search-button" aria-label="Replace All">All</button>
</div>
<!-- CodeMirror Area Wrapper -->
<div ref="cmEditorRef" class="cm-editor-wrapper rounded-b-md border border-gray-300 dark:border-gray-600 overflow-hidden" :class="{'editing-border': isEditing}" >
<!-- CodeMirror editor will be mounted here -->
</div>
</div>
<!-- == Execution Output (Common) == -->
<div v-if="executionOutput" class="mt-2" aria-live="polite">
<span class="text-lg font-semibold text-gray-700 dark:text-gray-300">Execution Output:</span>
<div class="execution-output-content hljs mt-1 p-2 rounded-md break-words text-sm leading-relaxed bg-white dark:bg-gray-800 max-h-48 overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary" v-html="sanitizedExecutionOutputHtml"></div>
</div>
</div>
</template>
<script>
import { nextTick } from 'vue';
import hljs from 'highlight.js';
import feather from 'feather-icons';
import DOMPurify from 'dompurify';
import { debounce } from 'lodash-es';
import { EditorView, keymap, lineNumbers, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, highlightActiveLineGutter } from "@codemirror/view";
import { EditorState, Compartment } from "@codemirror/state";
import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap, StreamLanguage } from "@codemirror/language";
import { defaultKeymap, history, historyKeymap, indentWithTab, undo as cmUndo, redo as cmRedo, historyField } from "@codemirror/commands";
// REMOVED searchState because it's not exported in your installed version
import { search, searchKeymap, findNext, findPrevious, replaceNext, replaceAll, SearchQuery, getSearchQuery, setSearchQuery } from "@codemirror/search";
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
import { lintKeymap } from "@codemirror/lint";
import { oneDark } from '@codemirror/theme-one-dark';
// Standard language packages
import { javascript } from "@codemirror/lang-javascript";
import { python } from "@codemirror/lang-python";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import { cpp } from "@codemirror/lang-cpp";
import { java } from "@codemirror/lang-java";
import { php } from "@codemirror/lang-php";
import { rust } from "@codemirror/lang-rust";
import { sql } from "@codemirror/lang-sql";
import { xml } from "@codemirror/lang-xml";
import { yaml } from "@codemirror/lang-yaml";
import { vue } from "@codemirror/lang-vue";
// Legacy language modes needed
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { go } from '@codemirror/legacy-modes/mode/go';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { lua } from '@codemirror/legacy-modes/mode/lua';
// Highlight.js CSS (for execution output)
import 'highlight.js/styles/github.css';
import 'highlight.js/styles/tokyo-night-dark.css';
const EXECUTABLE_LANGUAGES = new Set(['function', 'python', 'sh', 'shell', 'bash', 'cmd', 'powershell', 'latex', 'mermaid', 'graphviz', 'dot', 'javascript', 'html', 'html5', 'svg', 'lilypond']);
const NEW_TAB_EXECUTABLE_LANGUAGES = new Set(['airplay', 'mermaid', 'graphviz', 'dot', 'javascript', 'html', 'html5', 'svg', 'css']);
const VSCODE_SUPPORTED_LANGUAGES = new Set(['python', 'latex', 'vue', 'html', 'javascript', 'typescript', 'css', 'scss', 'less', 'json', 'yaml', 'markdown', 'java', 'csharp', 'php', 'ruby', 'go', 'rust', 'shell', 'bash', 'powershell']);
const INPUT_DEBOUNCE_MS = 300;
const SEARCH_UPDATE_DEBOUNCE_MS = 150;
function getHighlightedHtml(code, language) {
const codeToHighlight = typeof code === 'string' ? code : '';
try {
const effectiveLang = hljs.getLanguage(language) ? language : 'plaintext';
const result = hljs.highlight(codeToHighlight, { language: effectiveLang, ignoreIllegals: true });
return result.value.replace(/\n/g, '<br>');
} catch (e) {
console.warn("Highlighting error (output):", e, "Lang:", language);
const escaped = codeToHighlight.replace(/</g, "<").replace(/>/g, ">");
return escaped.replace(/\n/g, '<br>');
}
}
export default {
name: 'CodeBlock',
props: {
host: { type: String, required: false, default: "" },
language: { type: String, required: true },
code: { type: String, required: true },
client_id: { type: String, required: true },
discussion_id: { type: [String, Number], required: true },
message_id: { type: [String, Number], required: true },
},
emits: ['update-code'],
data() {
return {
isExecuting: false,
isCopied: false,
executionOutput: '',
copyTimeout: null,
isFunctionDetailsVisible: false,
isEditing: false,
isSearchVisible: false,
searchQuery: '',
replaceQuery: '',
cmView: null,
languageCompartment: new Compartment(),
editableCompartment: new Compartment(),
themeCompartment: new Compartment(),
updateListenerCompartment: new Compartment(),
debouncedEmitUpdate: null,
debouncedUpdateSearchQuery: null,
undoDepth: 0,
redoDepth: 0,
searchMatchCount: 0,
currentMatchIndex: -1, // 0-based index
darkModeObserver: null,
isDarkMode: false,
};
},
computed: {
safeCodeProp() { return typeof this.code === 'string' ? this.code : ''; },
safeLanguageProp() { return typeof this.language === 'string' ? this.language : ''; },
normalizedLanguage() { return this.safeLanguageProp.trim().toLowerCase(); },
isFunctionLanguage() { return this.normalizedLanguage === 'function'; },
cmLanguage() {
const lang = this.normalizedLanguage;
switch (lang) {
case 'python': case 'py': return python();
case 'javascript': case 'js': return javascript();
case 'typescript': case 'ts': return javascript({ typescript: true });
case 'jsx': return javascript({ jsx: true });
case 'tsx': return javascript({ jsx: true, typescript: true });
case 'html': case 'html5': return html();
case 'css': return css();
case 'json': return json();
case 'markdown': case 'md': return markdown();
case 'shell': case 'bash': case 'sh': case 'zsh': case 'cmd': case 'powershell': return shell();
case 'sql': return sql();
case 'yaml': case 'yml': return yaml();
case 'vue': case 'vue.js': return vue();
case 'java': return java();
case 'csharp': case 'cs': return cpp();
case 'c': case 'cpp': return cpp();
case 'php': return php();
case 'rust': case 'rs': return rust();
case 'xml': return xml();
case 'go': return StreamLanguage.define(go);
case 'ruby': case 'rb': return StreamLanguage.define(ruby);
case 'lua': return StreamLanguage.define(lua);
case 'latex':
case 'mermaid':
case 'graphviz':
case 'dot':
case 'lilypond':
case 'plaintext':
case 'text':
default: return null;
}
},
canExecute() { return EXECUTABLE_LANGUAGES.has(this.normalizedLanguage); },
canExecuteInNewTab() { return NEW_TAB_EXECUTABLE_LANGUAGES.has(this.normalizedLanguage); },
canOpenFolderInVsCode() { return VSCODE_SUPPORTED_LANGUAGES.has(this.normalizedLanguage); },
canOpenInVsCode() { return VSCODE_SUPPORTED_LANGUAGES.has(this.normalizedLanguage); },
effectiveLanguageDisplay() {
const lang = this.normalizedLanguage;
if (this.isFunctionLanguage) return 'json';
if (['shell', 'sh', 'bash', 'cmd', 'powershell'].includes(lang)) return 'shell';
if (lang === 'html5') return 'html';
if (lang === 'dot') return 'graphviz';
return this.cmLanguage ? lang : 'plaintext';
},
parsedFunctionCall() {
if (!this.isFunctionLanguage || !this.safeCodeProp) return null;
try {
const parsed = JSON.parse(this.safeCodeProp);
if (typeof parsed === 'object' && parsed !== null && typeof parsed.function_name === 'string' && parsed.function_name.trim() !== '' && typeof parsed.function_parameters === 'object' && parsed.function_parameters !== undefined) {
return parsed;
}
return null;
} catch (e) { return null; }
},
isValidFunctionCall() { return this.parsedFunctionCall !== null; },
functionName() { return this.parsedFunctionCall?.function_name ?? 'N/A'; },
functionParametersObject() { return this.parsedFunctionCall?.function_parameters ?? {}; },
hasParameters() { return Object.keys(this.functionParametersObject).length > 0; },
sanitizedExecutionOutputHtml() {
if (!this.executionOutput) return '';
const config = { USE_PROFILES: { html: true }, ADD_TAGS: ['iframe', 'svg', 'path', 'g', 'circle', 'rect', 'line', 'polyline', 'polygon', 'text', 'tspan', 'style', 'defs', 'marker', 'use', 'a'], ADD_ATTS: ['style', 'transform', 'cx', 'cy', 'r', 'x', 'y', 'width', 'height', 'fill', 'stroke', 'stroke-width', 'stroke-dasharray', 'points', 'd', 'marker-start', 'marker-end', 'viewBox', 'preserveAspectRatio', 'class', 'id', 'href', 'target', 'text-anchor', 'dominant-baseline', 'font-size', 'font-family', 'dy', 'aria-label'], ALLOW_DATA_ATTR: true, ALLOW_UNKNOWN_PROTOCOLS: false, FORBID_TAGS: ['script'], FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'] };
const looksLikeCode = !this.executionOutput.trim().startsWith('<');
const contentToSanitize = looksLikeCode
? getHighlightedHtml(this.executionOutput, 'plaintext')
: this.executionOutput;
return DOMPurify.sanitize(contentToSanitize, config);
},
copyIcon() { return this.isCopied ? 'check' : 'copy'; },
copyTitle() { return this.isCopied ? 'Copied!' : 'Copy code'; },
executeIcon() { return this.isExecuting ? 'loader' : 'play-circle'; },
executeTitle() { return this.isExecuting ? 'Executing...' : (this.isFunctionLanguage ? 'Execute Function Call' : 'Execute Code'); },
executeNewTabIcon() { return this.isExecuting ? 'loader' : 'airplay'; },
executeNewTabTitle() { return this.isExecuting ? 'Executing...' : 'Execute Code in New Tab'; },
canUndo() { return this.undoDepth > 0; },
canRedo() { return this.redoDepth > 0; },
hasMatches() { return this.searchMatchCount > 0; },
hasActiveMatch() { return this.currentMatchIndex >= 0 && this.currentMatchIndex < this.searchMatchCount; },
searchStatusText() {
if (!this.searchQuery) return ' \u00A0 ';
if (!this.hasMatches) return 'Not found';
if (this.hasActiveMatch) return `${this.currentMatchIndex + 1} / ${this.searchMatchCount}`;
return `${this.searchMatchCount} found`;
},
},
watch: {
code(newCode) {
const currentEditorCode = this.cmView ? this.cmView.state.doc.toString() : this.safeCodeProp; // Use prop as fallback if CM not ready
if (this.cmView && !this.isEditing && newCode !== currentEditorCode) {
this.updateEditorContent(newCode);
}
},
isEditing(editing) {
if (this.cmView) {
this.cmView.dispatch({
effects: this.editableCompartment.reconfigure(EditorView.editable.of(editing))
});
if (editing) {
nextTick(() => this.cmView?.focus());
}
}
},
cmLanguage(newLang) {
if (this.cmView) {
this.cmView.dispatch({
effects: this.languageCompartment.reconfigure(newLang ? [newLang] : [])
});
}
},
searchQuery() {
if (this.isEditing && this.isSearchVisible) {
this.debouncedUpdateSearchQuery();
}
},
replaceQuery() {
if (this.isEditing && this.isSearchVisible) {
this.debouncedUpdateSearchQuery();
}
},
isDarkMode(isDark) {
if (this.cmView) {
this.cmView.dispatch({
effects: this.themeCompartment.reconfigure(isDark ? oneDark : [])
});
this.cmView.dispatch({
effects: EditorView.theme({
".cm-gutters": {
backgroundColor: isDark ? "#374151" : "#f3f4f6",
color: isDark ? "#9ca3af" : "#6b7280",
borderRight: `1px solid ${isDark ? '#4b5563' : '#d1d5db'}`
},
}, {dark: isDark})
});
}
}
},
methods: {
triggerIconUpdate() {
nextTick(() => {
try { feather.replace(); } catch (e) { /* Ignore */ }
});
},
getActualCode() {
if (this.isFunctionLanguage) {
return this.safeCodeProp;
}
return this.cmView ? this.cmView.state.doc.toString() : this.safeCodeProp;
},
async copyCode() {
if (this.isCopied) return;
const codeToCopy = this.getActualCode();
try {
await navigator.clipboard.writeText(codeToCopy);
this.isCopied = true;
this.triggerIconUpdate();
if (this.copyTimeout) clearTimeout(this.copyTimeout);
this.copyTimeout = setTimeout(() => {
this.isCopied = false; this.triggerIconUpdate(); this.copyTimeout = null;
}, 1500);
} catch (err) { console.error('Failed to copy code:', err); alert('Error: Could not copy code.'); }
},
executeCodeInternal(endpointUrl, shouldOpenInNewTab = false) {
if (this.isExecuting) return;
this.isExecuting = true;
this.executionOutput = '';
this.triggerIconUpdate();
const currentCode = this.getActualCode();
const requestPayload = { client_id: this.client_id, code: currentCode, discussion_id: Number(this.discussion_id || 0), message_id: Number(this.message_id || 0), language: this.normalizedLanguage };
fetch(`${this.host}/${endpointUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/plain, */*' }, body: JSON.stringify(requestPayload) })
.then(async response => {
const contentType = response.headers.get("content-type");
let responseData;
if (contentType?.includes("application/json")) responseData = await response.json();
else responseData = { output: await response.text() };
if (!response.ok) {
let errorDetails = `HTTP error! Status: ${response.status}`;
if (responseData?.error) errorDetails += `, Message: ${responseData.error}`;
else if (typeof responseData?.output === 'string' && responseData.output.length > 0) errorDetails += `, Body: ${responseData.output.substring(0, 100)}...`;
else if (responseData?.detail) errorDetails += `, Detail: ${responseData.detail}`;
const error = new Error(errorDetails); error.response = responseData; throw error;
}
return responseData;
})
.then(data => {
if (typeof data?.output === 'string') this.executionOutput = data.output;
else if (typeof data?.message === 'string') this.executionOutput = data.message;
else if (data !== null && typeof data === 'object' && Object.keys(data).length > 0) {
try { this.executionOutput = JSON.stringify(data, null, 2); } catch { this.executionOutput = "[Object response]"; }
} else if (typeof data === 'string') this.executionOutput = data;
else this.executionOutput = "Execution successful (no specific output).";
if (shouldOpenInNewTab && data?.url) {
try { window.open(data.url, '_blank', 'noopener,noreferrer'); }
catch(e) { console.error("Failed to open URL:", e); this.executionOutput += `\n(Failed to open URL: ${data.url})`; }
}
})
.catch(error => { console.error('Code execution failed:', error); this.executionOutput = `Execution Error: ${error.message}`; })
.finally(() => {
this.isExecuting = false; this.triggerIconUpdate();
nextTick(() => { this.$el.querySelector('.execution-output-content')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); });
});
},
executeCode() { this.executeCodeInternal('execute_code', false); },
executeCode_in_new_tab() { this.executeCodeInternal('execute_code_in_new_tab', true); },
postRequest(endpointUrl, requestPayload = {}) {
const payloadToSend = { ...requestPayload, client_id: this.client_id, discussion_id: Number(this.discussion_id || 0) };
if (endpointUrl === 'open_code_in_vs_code') {
payloadToSend.code = this.getActualCode();
payloadToSend.message_id = Number(this.message_id || 0);
}
fetch(`${this.host}/${endpointUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payloadToSend) })
.then(async response => {
if (!response.ok) {
let errorDetail = `HTTP ${response.status}`; try { const eb = await response.json(); errorDetail += `: ${eb.detail || JSON.stringify(eb)}`; } catch (e) { errorDetail += ` (${response.statusText})`; } throw new Error(errorDetail);
}
const contentType = response.headers.get("content-type");
return (contentType?.includes("application/json")) ? response.json() : {};
})
.then(data => { /* Optional success handling */ })
.catch(error => { console.error(`Fetch error during ${endpointUrl}:`, error); alert(`Operation failed: ${error.message}`); });
},
openFolderVsCode() { this.postRequest('open_discussion_folder_in_vs_code'); },
openVsCode() { this.postRequest('open_code_in_vs_code'); },
openFolder() { this.postRequest('open_discussion_folder'); },
toggleFunctionDetails() { this.isFunctionDetailsVisible = !this.isFunctionDetailsVisible; this.triggerIconUpdate(); },
updateEditorContent(newCode) {
if (!this.cmView || this.isFunctionLanguage) return;
const currentDoc = this.cmView.state.doc.toString();
if (newCode !== currentDoc) {
this.cmView.dispatch({
changes: { from: 0, to: currentDoc.length, insert: newCode }
});
}
},
createUpdateListener() {
// Modified listener: Removed access to the unexported searchState field
return EditorView.updateListener.of((update) => {
// Update history state for button disabling (This part is OK)
const histState = update.state.field(historyField, false);
if (histState) {
this.undoDepth = histState.done.length;
this.redoDepth = histState.undone.length;
}
// --- REMOVED SECTION START ---
// The following block is removed because 'searchState' is not exported
// in the user's installed version, making access via update.state.field() impossible.
/*
const currentSearchStateField = update.state.field(searchState, false); // <--- PROBLEM LINE
if (currentSearchStateField) {
const currentCmQuery = getSearchQuery(update.state);
const cmQuerySpec = currentCmQuery ? SearchQuery.fromJSON(currentCmQuery).spec : null;
if (cmQuerySpec?.search && cmQuerySpec.search === this.searchQuery) {
this.searchMatchCount = currentSearchStateField.count; // <--- Depends on removed line
const selection = update.state.selection.main;
if (!selection.empty) {
const matches = Array.from(currentSearchStateField.matches); // <--- Depends on removed line
const idx = matches.findIndex(m => m.from === selection.from && m.to === selection.to);
this.currentMatchIndex = idx;
} else if (this.hasMatches && this.currentMatchIndex === -1) {
// Keep index if selection is cursor and no specific match is selected
} else if (!this.hasMatches) {
this.currentMatchIndex = -1;
}
} else if (!cmQuerySpec?.search && !this.searchQuery) {
this.searchMatchCount = 0;
this.currentMatchIndex = -1;
}
} else {
this.searchMatchCount = 0;
this.currentMatchIndex = -1;
}
*/
// --- REMOVED SECTION END ---
// We can still manually reset counts when the query becomes empty,
// but we can't reliably get the count or index otherwise with this old version.
if (!this.searchQuery && this.isSearchVisible) {
this.searchMatchCount = 0;
this.currentMatchIndex = -1;
}
// It might be better to update searchMatchCount/currentMatchIndex ONLY
// when findNext/findPrevious/updateSearchQueryState are called,
// as we can't reliably track it via the state field in the listener.
// For now, just handling the empty query case here.
// Emit update if document changed by user interaction (This part is OK)
if (update.docChanged && this.isEditing) {
this.debouncedEmitUpdate(update.state.doc.toString());
}
});
},
setupCodeMirror() {
if (!this.$refs.cmEditorRef || this.isFunctionLanguage) return;
const state = EditorState.create({
doc: this.safeCodeProp,
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter({ markerDOM: (open) => { const m = document.createElement('span'); m.textContent = open ? '▾' : '▸'; m.style.cursor = 'pointer'; return m; } }),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
search({ top: false, createPanel() { return { dom: document.createElement('div'), top: false }; } }),
keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, indentWithTab ]),
this.languageCompartment.of(this.cmLanguage ? [this.cmLanguage] : []),
this.editableCompartment.of(EditorView.editable.of(this.isEditing)),
this.themeCompartment.of(this.isDarkMode ? oneDark : []),
this.updateListenerCompartment.of(this.createUpdateListener()),
EditorView.theme({
"&": { fontSize: "0.875rem", },
".cm-scroller": { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', overflowX: "auto" },
".cm-content": { caretColor: "currentColor", padding: "0.5rem" },
".cm-gutters": { backgroundColor: this.isDarkMode ? "#374151" : "#f3f4f6", color: this.isDarkMode ? "#9ca3af" : "#6b7280", borderRight: `1px solid ${this.isDarkMode ? '#4b5563' : '#d1d5db'}` },
"&.cm-focused": { outline: "none", },
".cm-foldMarker": { cursor: "pointer", color: this.isDarkMode ? "#9ca3af" : "#6b7280", padding: "0 2px" },
".cm-selectionBackground": { backgroundColor: this.isDarkMode ? "#354b6d !important" : "#dbeafe !important" },
".cm-cursor": { borderLeftColor: "currentColor" },
".cm-searchMatch": { backgroundColor: this.isDarkMode ? "#facc15aa" : "#fef08aaa" },
".cm-searchMatch-selected": { backgroundColor: this.isDarkMode ? "#fbbf24cc" : "#fde047cc" }
})
]
});
this.cmView = new EditorView({
state,
parent: this.$refs.cmEditorRef
});
},
destroyCodeMirror() {
if (this.cmView) {
this.cmView.destroy();
this.cmView = null;
}
},
toggleEditMode() {
if (this.isFunctionLanguage) return;
this.isEditing = !this.isEditing;
if (!this.isEditing) {
this.isSearchVisible = false;
this.debouncedEmitUpdate.flush();
} else {
nextTick(() => {
this.cmView?.focus();
const histState = this.cmView?.state.field(historyField, false);
if (histState) {
this.undoDepth = histState.done.length;
this.redoDepth = histState.undone.length;
}
});
}
this.triggerIconUpdate();
},
undo() { if (this.cmView && this.canUndo) { cmUndo(this.cmView); this.cmView.focus(); } },
redo() { if (this.cmView && this.canRedo) { cmRedo(this.cmView); this.cmView.focus(); } },
toggleSearch() {
if (!this.cmView) return;
this.isSearchVisible = !this.isSearchVisible;
if (this.isSearchVisible) {
nextTick(() => {
this.$refs.searchInputRef?.focus();
this.debouncedUpdateSearchQuery();
});
} else {
setSearchQuery(this.cmView, new SearchQuery({ search: '' }));
this.searchQuery = '';
this.replaceQuery = '';
}
this.triggerIconUpdate();
},
findNextAndHighlight() { if (this.cmView && this.hasMatches) { findNext(this.cmView); this.cmView.focus(); } },
findPreviousAndHighlight() { if (this.cmView && this.hasMatches) { findPrevious(this.cmView); this.cmView.focus(); } },
replaceCurrent() { if (this.cmView && this.hasActiveMatch) { replaceNext(this.cmView); this.cmView.focus(); } },
replaceCurrentAndFindNext() { this.replaceCurrent(); },
replaceAllMatches() { if (this.cmView && this.hasMatches) { replaceAll(this.cmView); this.cmView.focus(); } },
updateSearchQueryState() {
if (!this.cmView || !this.isSearchVisible) return;
setSearchQuery(this.cmView, new SearchQuery({
search: this.searchQuery,
replace: this.replaceQuery,
caseSensitive: false
}));
if (this.searchQuery) {
findNext(this.cmView); // Trigger search execution
} else {
// If query is cleared, reset counts
this.searchMatchCount = 0;
this.currentMatchIndex = -1;
}
},
observeDarkMode() {
this.isDarkMode = document.documentElement.classList.contains('dark');
this.darkModeObserver = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
this.isDarkMode = document.documentElement.classList.contains('dark');
break;
}
}
});
this.darkModeObserver.observe(document.documentElement, { attributes: true });
}
},
created() {
this.debouncedEmitUpdate = debounce((code) => {
this.$emit('update-code', code);
}, INPUT_DEBOUNCE_MS);
this.debouncedUpdateSearchQuery = debounce(this.updateSearchQueryState, SEARCH_UPDATE_DEBOUNCE_MS);
},
mounted() {
if (!this.isFunctionLanguage) {
this.observeDarkMode();
nextTick(() => {
this.setupCodeMirror();
this.triggerIconUpdate();
});
} else {
this.triggerIconUpdate();
}
},
beforeUnmount() {
this.destroyCodeMirror();
if (this.copyTimeout) clearTimeout(this.copyTimeout);
this.debouncedEmitUpdate?.cancel();
this.debouncedUpdateSearchQuery?.cancel();
this.darkModeObserver?.disconnect();
},
updated() {
this.triggerIconUpdate();
}
};
</script>
<style>
</style>

View File

@ -1,285 +0,0 @@
<template>
<div
:class="selected ? 'discussion-hilighted' : 'discussion'"
class="m-1 py-2 flex flex-row sm:flex-row flex-wrap flex-shrink-0 items-center rounded-md duration-75 cursor-pointer relative w-[15rem]"
:id="'dis-' + id"
@click.stop="selectEvent()"
>
<!-- PRE TITLE SECTION -->
<div class="flex flex-row items-center gap-2">
<!-- CHECKBOX -->
<div v-if="isCheckbox">
<input
type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
@click.stop
v-model="checkBoxValue_local"
@input="checkedChangeEvent($event, id)"
/>
</div>
<!-- INDICATOR FOR SELECTED ITEM -->
<div
v-if="selected"
class="min-h-full w-2 rounded-xl self-stretch"
:class="loading ? 'animate-bounce bg-accent' : 'bg-secondary'"
></div>
<div
v-if="!selected"
class="w-2"
:class="loading ? 'min-h-full w-2 rounded-xl self-stretch animate-bounce bg-accent' : ''"
></div>
</div>
<!-- CONTAINER FOR TITLE -->
<div class="flex flex-row items-center w-full">
<!-- TITLE -->
<p
v-if="!editTitle"
:title="title"
class="line-clamp-1 w-full ml-1 -mx-5 text-xs"
>
{{
title
? title === 'untitled'
? 'New discussion'
: title
: 'New discussion'
}}
</p>
<input
v-if="editTitle"
type="text"
id="title-box"
ref="titleBox"
class="bg-bg-light dark:bg-bg-dark rounded-md border-0 w-full -m-1 p-1"
:value="title"
required
@keydown.enter.exact="editTitleEvent()"
@keydown.esc.exact="editTitleMode = false"
@input="chnageTitle($event.target.value)"
@click.stop
/>
</div>
<!-- CONTROL BUTTONS AS SLIDING FLOATING MENU -->
<div
class="absolute top-0 right-0 h-full flex items-center group"
>
<div
class="discussion-toolbox"
>
<!-- EDIT TITLE CONFIRM -->
<div v-if="showConfirmation" class="flex gap-2 items-center">
<button
class="text-2xl hover:text-red-600 duration-75 active:scale-90"
title="Discard title changes"
type="button"
@click.stop="cancel()"
>
<i data-feather="x"></i>
</button>
<button
class="text-2xl hover:text-secondary duration-75 active:scale-90"
title="Confirm title changes"
type="button"
@click.stop="editTitleMode ? editTitleEvent() : deleteMode ? deleteEvent() : makeTitleEvent()"
>
<i data-feather="check"></i>
</button>
</div>
<!-- EDIT AND REMOVE -->
<div v-if="!showConfirmation" class="flex gap-2 items-center">
<button v-if="openfolder_enabled"
class="text-2xl hover:text-secondary duration-75 active:scale-90"
title="Open folder"
type="button"
@click.stop="openFolderEvent()"
>
<i data-feather="folder"></i>
</button>
<button
class="text-2xl hover:text-secondary duration-75 active:scale-90"
title="Make a title"
type="button"
@click.stop="makeTitleMode = true"
>
<i data-feather="type"></i>
</button>
<button
class="text-2xl hover:text-secondary duration-75 active:scale-90"
title="Edit title"
type="button"
@click.stop="editTitleMode = true"
>
<i data-feather="edit-2"></i>
</button>
<button
class="text-2xl hover:text-red-600 duration-75 active:scale-90"
title="Remove discussion"
type="button"
@click.stop="deleteMode = true"
>
<i data-feather="trash"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { nextTick } from 'vue'
import feather from 'feather-icons'
export default {
name: 'Discussion',
emits: ['delete', 'select', 'openFolder', 'editTitle', 'makeTitle', 'checked'],
props: {
id: Number,
title: String,
selected: Boolean,
loading: Boolean,
isCheckbox: Boolean,
checkBoxValue: Boolean,
openfolder_enabled: Boolean
},
setup() {
},
data() {
return {
showConfirmation: false,
editTitleMode: false,
makeTitleMode: false,
deleteMode:false,
openFolder:false,
editTitle: false,
newTitle: String,
checkBoxValue_local: false
}
},
methods: {
cancel(){
this.editTitleMode = false
this.makeTitleMode = false
this.deleteMode = false
this.showConfirmation = false
},
deleteEvent() {
this.showConfirmation = false
this.$emit("delete")
},
selectEvent() {
this.$emit("select")
},
openFolderEvent() {
this.$emit("openFolder",
{
id: this.id
})
},
editTitleEvent() {
this.editTitle = false
this.editTitleMode = false
this.makeTitleMode = false
this.deleteMode = false
this.showConfirmation = false
this.$emit("editTitle",
{
title: this.newTitle,
id: this.id
})
},
makeTitleEvent(){
this.$emit("makeTitle",
{
id: this.id
})
this.showConfirmation = false
},
chnageTitle(text) {
this.newTitle = text
},
checkedChangeEvent(event, id) {
this.$emit("checked", event, id)
}
},
mounted() {
this.newTitle = this.title
nextTick(() => {
feather.replace()
})
}, watch: {
showConfirmation() {
nextTick(() => {
feather.replace()
})
},
editTitleMode(newval) {
this.showConfirmation = newval
this.editTitle = newval
if (newval) {
nextTick(() => {
try{
this.$refs.titleBox.focus()
}catch{}
})
}
},
deleteMode(newval) {
this.showConfirmation = newval
if (newval) {
nextTick(() => {
this.$refs.titleBox.focus()
})
}
},
makeTitleMode(newval) {
this.showConfirmation = newval
},
checkBoxValue(newval, oldval) {
this.checkBoxValue_local = newval
}
}
}
</script>
<style scoped>
/* Style for the control buttons container */
.control-buttons {
position: absolute;
top: 0;
right: 0;
height: 100%;
display: flex;
align-items: center;
transform: translateX(100%);
transition: transform 0.3s;
}
.group:hover .control-buttons {
transform: translateX(0);
}
.control-buttons-inner {
display: flex;
gap: 10px;
align-items: center;
background-color: white; /* or your desired color */
padding: 8px;
border-radius: 0 0 0 8px; /* Rounded left corners */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -1,67 +0,0 @@
<template>
<div :id="containerId" ref="container"></div>
</template>
<script>
export default {
name: 'DynamicUIRenderer',
props: {
ui: {
type: String,
required: true
},
instanceId: {
type: String,
required: true
}
},
data() {
return {
containerId: `dynamic-ui-${this.instanceId}`
};
},
watch: {
ui: {
immediate: true,
handler(newValue) {
console.log(`UI prop changed for instance ${this.instanceId}:`, newValue);
this.$nextTick(() => {
this.renderContent();
});
}
}
},
methods: {
renderContent() {
console.log(`Rendering content for instance ${this.instanceId}...`);
const container = this.$refs.container;
// Parse the UI string
const parser = new DOMParser();
const doc = parser.parseFromString(this.ui, 'text/html');
// Extract and inject CSS
const styles = doc.getElementsByTagName('style');
Array.from(styles).forEach(style => {
const scopedStyle = document.createElement('style');
scopedStyle.textContent = this.scopeCSS(style.textContent);
document.head.appendChild(scopedStyle);
});
// Extract and inject HTML
container.innerHTML = doc.body.innerHTML;
// Extract and execute JavaScript
const scripts = doc.getElementsByTagName('script');
Array.from(scripts).forEach(script => {
const newScript = document.createElement('script');
newScript.textContent = script.textContent;
container.appendChild(newScript);
});
},
scopeCSS(css) {
return css.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g, `#${this.containerId} $1$2`);
}
}
};
</script>

View File

@ -1,134 +0,0 @@
<!-- JsonNode.vue -->
<template>
<div class="json-tree">
<!-- Object -->
<div v-if="isObject" class="tree-node">
<div class="node-label" @click="toggle">
<span class="toggle-icon">{{ expanded ? '▼' : '▶' }}</span>
<span class="key">{{ label }}</span>
<span class="bracket">{{ isArray ? '[' : '{' }}</span>
</div>
<div v-if="expanded" class="node-content">
<div v-for="(value, key) in data" :key="key" class="node-item">
<json-node
:data="value"
:label="key"
:depth="depth + 1"
/>
</div>
</div>
<div v-if="expanded" class="bracket-close">{{ isArray ? ']' : '}' }}</div>
</div>
<!-- Primitive values -->
<div v-else class="tree-leaf">
<span class="key" v-if="label">{{ label }}:</span>
<span :class="['value', getValueType(data)]">{{ formatValue(data) }}</span>
</div>
</div>
</template>
<script>
export default {
name: 'JsonNode',
props: {
data: {
required: true
},
label: {
type: String,
default: ''
},
depth: {
type: Number,
default: 0
}
},
data() {
return {
expanded: true
}
},
computed: {
isObject() {
return this.data !== null && typeof this.data === 'object'
},
isArray() {
return Array.isArray(this.data)
}
},
methods: {
toggle() {
this.expanded = !this.expanded
},
getValueType(value) {
if (value === null) return 'null'
return typeof value
},
formatValue(value) {
if (value === null) return 'null'
if (typeof value === 'string') return `"${value}"`
return value
}
}
}
</script>
<style scoped>
.json-tree {
font-family: monospace;
font-size: 14px;
line-height: 1.5;
margin-left: 20px;
}
.tree-node {
position: relative;
}
.node-label {
cursor: pointer;
padding: 2px 0;
}
.node-label:hover {
background-color: #f0f0f0;
}
.toggle-icon {
display: inline-block;
width: 20px;
color: #666;
}
.node-content {
border-left: 1px dotted #ccc;
margin-left: 7px;
padding-left: 13px;
}
.key {
color: #881391;
margin-right: 5px;
}
.value {
padding: 2px 4px;
}
.value.string { color: #22863a; }
.value.number { color: #005cc5; }
.value.boolean { color: #d73a49; }
.value.null { color: #6a737d; }
.bracket {
color: #444;
margin-left: 5px;
}
.bracket-close {
color: #444;
margin-left: 7px;
}
</style>

View File

@ -1,87 +0,0 @@
<!-- JsonViewer.vue -->
<template>
<div class="json-viewer">
<div class="viewer-header" @click="toggle">
<span class="toggle-icon">{{ expanded ? '▼' : '▶' }}</span>
<span class="title">{{ title }}</span>
</div>
<div v-if="expanded" class="viewer-content">
<json-node :data="parsedData" />
</div>
</div>
</template>
<script>
import JsonNode from './JsonNode.vue'
export default {
name: 'JsonViewer',
components: { JsonNode },
props: {
data: {
required: true
},
title: {
type: String,
default: 'JSON Data'
}
},
data() {
return {
expanded: true
}
},
computed: {
parsedData() {
if (typeof this.data === 'string') {
try {
return JSON.parse(this.data)
} catch (e) {
return { error: 'Invalid JSON' }
}
}
return this.data
}
},
methods: {
toggle() {
this.expanded = !this.expanded
}
}
}
</script>
<style scoped>
.json-viewer {
border: 1px solid #ddd;
border-radius: 4px;
margin: 10px;
background: white;
}
.viewer-header {
padding: 8px 12px;
background: #f5f5f5;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
.viewer-header:hover {
background: #eee;
}
.toggle-icon {
display: inline-block;
width: 20px;
color: #666;
}
.title {
font-weight: bold;
color: #333;
}
.viewer-content {
padding: 10px;
}
</style>

View File

@ -1,552 +0,0 @@
<template>
<div class="break-all container w-full">
<div ref="mdRender" class="markdown-content">
<div v-for="(item, index) in markdownItems" :key="index">
<code-block
v-if="item.type === 'code'"
:host="host"
:language="item.language"
:code="item.code"
:discussion_id="discussion_id"
:message_id="message_id"
:client_id="client_id"
@update-code="updateCode(index, $event)"
></code-block>
<thinking-block
v-else-if="item.type === 'thinking'"
:content="item.content"
:is-done="item.is_done"
></thinking-block>
<latex-editor
v-else-if="item.type === 'latex'"
:initial-latex-code="item.code"
:inline="item.inline"
@update:latexCode="updateLatex(index, $event)"
class="my-1"
></latex-editor>
<div v-else v-html="item.html"></div>
</div>
</div>
</div>
</template>
<script>
import { nextTick } from 'vue';
import feather from 'feather-icons';
import MarkdownIt from 'markdown-it';
import emoji from 'markdown-it-emoji';
import anchor from 'markdown-it-anchor';
import implicitFigures from 'markdown-it-implicit-figures';
import 'highlight.js/styles/tokyo-night-dark.css';
import attrs from 'markdown-it-attrs';
import CodeBlock from './CodeBlock.vue';
import ThinkingBlock from './ThinkingBlock.vue';
import LatexEditor from './LatexEditor.vue';
import hljs from 'highlight.js';
import mathjax from 'markdown-it-mathjax3';
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, "\"")
.replace(/'/g, "'");
}
function findNextTag(state, startLine) {
for (let i = startLine; i < state.lineMax; i++) {
let line = state.src.slice(state.bMarks[i], state.eMarks[i]).trim();
if (line === '<thinking>' || line === '<think>' || line === '</thinking>' || line === '</think>') {
return { line: i, tag: line };
}
}
return null;
}
const thinkingRule = (state, startLine, endLine, silent) => {
let startPos = state.bMarks[startLine] + state.tShift[startLine];
let maxPos = state.eMarks[startLine];
let lineTextTrimmed = state.src.slice(startPos, maxPos).trim();
let isExplicitStart = lineTextTrimmed === '<thinking>' || lineTextTrimmed === '<think>';
let nextTagInfo = findNextTag(state, startLine + (isExplicitStart ? 1 : 0));
let isImplicitStart = !isExplicitStart && nextTagInfo && (nextTagInfo.tag === '</thinking>' || nextTagInfo.tag === '</think>');
if (isExplicitStart || isImplicitStart) {
let startTag = isExplicitStart ? lineTextTrimmed : (nextTagInfo.tag === '</thinking>' ? '<thinking>' : '<think>');
let endTag = startTag.replace('<', '</');
let contentLines = [];
let contentStartLine = startLine + (isExplicitStart ? 1 : 0);
let blockEndLine = endLine;
let foundEndTag = false;
let currentLineIdx = contentStartLine;
while (currentLineIdx < endLine) {
let currentLineRaw = state.src.slice(state.bMarks[currentLineIdx], state.eMarks[currentLineIdx]);
let currentLineTrimmed = currentLineRaw.trim();
if (isExplicitStart && currentLineTrimmed === endTag) {
foundEndTag = true;
blockEndLine = currentLineIdx + 1;
break;
}
if (isImplicitStart && currentLineIdx === nextTagInfo.line) {
foundEndTag = true;
blockEndLine = currentLineIdx + 1;
break;
}
if( (!isExplicitStart || currentLineIdx < endLine) && (!isImplicitStart || currentLineIdx < nextTagInfo.line) ){
contentLines.push(currentLineRaw);
}
currentLineIdx++;
}
if (isImplicitStart) {
blockEndLine = nextTagInfo.line + 1;
} else if (!foundEndTag){
blockEndLine = currentLineIdx;
}
let isDone = (isExplicitStart && foundEndTag) || isImplicitStart;
if (silent) return true;
let rawBlockContent = state.src.slice(state.bMarks[startLine], state.eMarks[blockEndLine - 1]);
let innerContent = contentLines.join('\n');
let token = state.push('thinking_open', 'div', 1);
token.markup = startTag;
token.block = true;
token.is_done = isDone;
token.implicit = isImplicitStart;
token.map = [startLine, blockEndLine];
token.meta = { rawBlock: rawBlockContent, innerContent: innerContent };
token = state.push('thinking_content', '', 0);
token.content = innerContent;
token.is_done = isDone;
token = state.push('thinking_close', 'div', -1);
token.markup = endTag;
token.block = true;
token.is_done = isDone;
state.line = blockEndLine;
return true;
}
return false;
};
export default {
name: 'MarkdownRenderer',
components: {
CodeBlock,
ThinkingBlock,
LatexEditor,
},
props: {
host: {
type: String,
required: false,
default: "http://localhost:9600",
},
client_id: {
type: String,
required: true,
},
markdownText: {
type: String,
required: true,
},
discussion_id: {
type: [String, Number],
default: "0",
required: false,
},
message_id: {
type: [String, Number],
default: "0",
required: false,
},
},
emits: ['update:markdownText'],
data() {
return {
markdownItems: [],
md: null,
_isUpdatingInternally: false,
};
},
watch: {
markdownText(newValue) {
if (!this._isUpdatingInternally) {
this.parseAndRenderMarkdown();
}
this._isUpdatingInternally = false;
}
},
created() {
this.md = new MarkdownIt({
html: false,
breaks: true,
highlight: (code, language) => {
const validLanguage = language && hljs.getLanguage(language) ? language : 'plaintext';
try {
const result = hljs.highlight(validLanguage, code, true);
return `<pre class="hljs"><code>${result.value}</code></pre>`;
} catch (__) {
return `<pre class="hljs"><code>${escapeHtml(code)}</code></pre>`;
}
},
})
.use(emoji)
.use(anchor)
.use(implicitFigures, { figcaption: true })
.use(attrs)
.use(mathjax);
this.md.renderer.rules.fence = () => '';
this.md.renderer.rules.thinking_open = () => '';
this.md.renderer.rules.thinking_content = () => '';
this.md.renderer.rules.thinking_close = () => '';
this.md.renderer.rules.math_inline = () => '';
this.md.renderer.rules.math_block = () => '';
this.md.block.ruler.before('fence', 'thinking', thinkingRule);
},
mounted() {
this.parseAndRenderMarkdown();
},
methods: {
// Inside MarkdownRenderer.vue methods:
getRawMarkdownChunk(startLine, endLine) {
if (typeof startLine !== 'number' || typeof endLine !== 'number' || startLine < 0 || endLine <= startLine) {
// console.warn(`getRawMarkdownChunk called with invalid lines: ${startLine}, ${endLine}`);
return '';
}
const lines = this.markdownText.split(/\r?\n/);
const safeStartLine = Math.min(startLine, lines.length);
const safeEndLine = Math.min(endLine, lines.length);
if (safeStartLine >= safeEndLine) return '';
// Slice captures lines from start index up to, but not including, end index.
return lines.slice(safeStartLine, safeEndLine).join('\n');
},
parseAndRenderMarkdown() {
if (!this.markdownText || !this.md) {
this.markdownItems = [];
return;
}
try {
const tokens = this.md.parse(this.markdownText, {});
const newItems = [];
let lastProcessedLine = 0;
const totalLines = this.markdownText.split(/\r?\n/).length;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
// Skip tokens that don't represent distinct blocks or are handled implicitly
if (!token.map || !Array.isArray(token.map) || token.map.length < 2) continue;
// Skip closing tags for blocks we handle explicitly by their opening tag
if (token.type === 'thinking_close' || token.type === 'thinking_content') continue;
// Skip inline math handled by math_block/math_inline rules already
// if (token.type === 'math_inline_double' || token.type === 'math_block_label') continue;
const startLine = token.map[0];
let endLine = token.map[1]; // Use let for potential adjustment
// --- Logic ---
// 1. Render any standard markdown *before* this token's block starts
if (startLine > lastProcessedLine) {
const rawChunk = this.getRawMarkdownChunk(lastProcessedLine, startLine);
if (rawChunk) { // Add even if just whitespace, preserving structure
newItems.push({
type: 'markdown',
raw: rawChunk,
html: this.md.render(rawChunk) // Render only this chunk
});
}
lastProcessedLine = startLine; // Advance past the rendered gap
} else if (startLine < lastProcessedLine) {
// Overlap situation, likely means this token is inside a block already processed.
// Safest is often to skip it to avoid duplication.
continue;
}
// 2. Identify and process the *current* block based on the token type
let blockProcessed = false;
if (token.type === 'thinking_open') {
// Look ahead for content and close tokens to get the full range & data
let thinkingEndLine = endLine;
let thinkingContent = '';
let isDone = false; // Assume not done unless close tag found
let consumedTokens = 0;
if (i + 1 < tokens.length && tokens[i+1].type === 'thinking_content') {
thinkingContent = tokens[i+1].content;
consumedTokens = 1;
if(tokens[i+1].map) thinkingEndLine = Math.max(thinkingEndLine, tokens[i+1].map[1]);
if (i + 2 < tokens.length && tokens[i+2].type === 'thinking_close') {
isDone = tokens[i+2].is_done !== undefined ? tokens[i+2].is_done : true; // Default to true if closed
consumedTokens = 2;
if(tokens[i+2].map) thinkingEndLine = Math.max(thinkingEndLine, tokens[i+2].map[1]);
}
}
const rawContent = this.getRawMarkdownChunk(startLine, thinkingEndLine);
newItems.push({
type: 'thinking',
raw: rawContent,
content: thinkingContent,
is_done: isDone,
implicit: token.implicit || false,
});
lastProcessedLine = thinkingEndLine;
i += consumedTokens; // Skip the consumed content/close tokens
blockProcessed = true;
} else if (token.type === 'fence') {
const rawFence = this.getRawMarkdownChunk(startLine, endLine);
newItems.push({
type: 'code',
raw: rawFence,
language: escapeHtml(token.info.trim()),
code: token.content,
});
lastProcessedLine = endLine;
blockProcessed = true;
} else if (token.type === 'math_inline' || token.type === 'math_block') {
const isInline = token.type === 'math_inline';
const delimiter = isInline ? '$' : '$$';
const rawLatex = token.markup && token.content ? `${delimiter}${token.content}${delimiter}` : this.getRawMarkdownChunk(startLine, endLine);
newItems.push({
type: 'latex',
raw: rawLatex,
code: token.content,
inline: isInline,
});
lastProcessedLine = endLine;
blockProcessed = true;
} else if (token.level === 0 && !token.hidden && token.type.endsWith('_open')) {
// --- Potential Standard Markdown Block Start ---
// This is a heuristic: identifies top-level opening tags like paragraph_open, list_item_open, etc.
// We need to find the corresponding closing tag to get the full range.
let blockEndLine = endLine;
let nestingLevel = 0;
let j = i + 1;
for (; j < tokens.length; j++) {
const innerToken = tokens[j];
if(innerToken.type === token.type) { // Same opening tag type
nestingLevel++;
} else if (innerToken.type === token.type.replace('_open', '_close')) {
if (nestingLevel === 0) {
blockEndLine = innerToken.map ? Math.max(endLine, innerToken.map[1]) : endLine;
break; // Found the matching close tag
} else {
nestingLevel--;
}
}
// Make sure endLine tracks the maximum extent within the block
if(innerToken.map) blockEndLine = Math.max(blockEndLine, innerToken.map[1]);
}
// Now we have the range [startLine, blockEndLine] for this standard block
const rawChunk = this.getRawMarkdownChunk(startLine, blockEndLine);
if (rawChunk.trim()) { // Only add if it has content
newItems.push({
type: 'markdown',
raw: rawChunk,
html: this.md.render(rawChunk)
});
lastProcessedLine = blockEndLine;
i = j; // Skip all tokens within this processed block
blockProcessed = true;
} else if (rawChunk) { // Preserve whitespace blocks if necessary
newItems.push({ type: 'markdown', raw: rawChunk, html: '' });
lastProcessedLine = blockEndLine;
i = j;
blockProcessed = true;
} else {
// Empty block, just advance lastProcessedLine if needed
lastProcessedLine = Math.max(lastProcessedLine, blockEndLine);
}
}
// 3. If no block was explicitly processed, but the token advanced lines, update lastProcessedLine.
// This catches simple cases or tokens missed by the block logic.
if (!blockProcessed && endLine > lastProcessedLine) {
// This path should ideally be hit less often with the block detection above.
// Could render the single token's range as markdown if needed, but might cause issues.
// For now, just advance the line counter.
lastProcessedLine = endLine;
}
} // End for loop
// 4. Handle any final trailing markdown chunk after the last processed token/block.
if (lastProcessedLine < totalLines) {
const rawChunk = this.getRawMarkdownChunk(lastProcessedLine, totalLines);
if (rawChunk) { // Add if non-empty (including whitespace)
newItems.push({
type: 'markdown',
raw: rawChunk,
html: this.md.render(rawChunk),
});
}
}
this.markdownItems = newItems;
} catch (error) {
console.error("Error parsing markdown:", error);
// Fallback: Render the whole thing simply in case of error
this.markdownItems = [{ type: 'markdown', raw: this.markdownText, html: this.md.render(this.markdownText) }];
} finally {
nextTick(() => {
feather.replace();
// MathJax if needed
});
}
},
updateCode(index, newCode) {
if (index >= 0 && index < this.markdownItems.length && this.markdownItems[index]?.type === 'code') {
const item = this.markdownItems[index];
item.code = newCode;
const lang = item.language || '';
const tick = '```';
item.raw = `${tick}${lang}\n${newCode}\n${tick}`;
const newMarkdownText = this.reconstructMarkdown();
this._isUpdatingInternally = true;
this.$emit('update:markdownText', newMarkdownText);
} else {
console.warn(`updateCode called with invalid index ${index} or item type`);
}
},
updateLatex(index, newLatexCode) {
if (index >= 0 && index < this.markdownItems.length && this.markdownItems[index]?.type === 'latex') {
const item = this.markdownItems[index];
item.code = newLatexCode;
const delimiter = item.inline ? '$' : '$$';
item.raw = `${delimiter}${newLatexCode}${delimiter}`;
const newMarkdownText = this.reconstructMarkdown();
this._isUpdatingInternally = true;
this.$emit('update:markdownText', newMarkdownText);
} else {
console.warn(`updateLatex called with invalid index ${index} or item type`);
}
},
reconstructMarkdown() {
return this.markdownItems.map(item => item.raw).join('');
},
}
};
</script>
<style scoped>
.markdown-content :deep(code:not(pre code)) {
background-color: #f0f0f0;
padding: 0.2em 0.4em;
margin: 0 0.1em;
font-size: 85%;
border-radius: 3px;
color: #333;
word-break: break-word;
}
.markdown-content :deep(pre.hljs) {
padding: 1em;
margin: 1em 0;
overflow-x: auto;
border-radius: 6px;
background-color: #2a2734;
}
.markdown-content :deep(pre.hljs code) {
background-color: transparent;
padding: 0;
margin: 0;
font-size: inherit;
border-radius: 0;
color: inherit;
white-space: pre;
word-break: normal;
}
.markdown-content :deep(.thinking-block) {
border-left: 3px solid orange;
padding: 0.5em 1em;
margin: 1em 0;
background-color: #fff8e1;
opacity: 0.8;
transition: opacity 0.3s ease-in-out;
border-radius: 0 4px 4px 0;
}
.markdown-content :deep(.thinking-block[data-done="true"]) {
opacity: 1;
border-left-color: #4caf50;
background-color: #e8f5e9;
}
.markdown-content :deep(.thinking-content) {
white-space: pre-wrap;
font-style: italic;
color: #616161;
}
.markdown-content :deep(p) {
margin-bottom: 1rem;
}
/* Reduce margin for paragraphs immediately followed by LatexEditor */
.markdown-content :deep(p + .latex-editor-container) {
margin-top: -0.5rem; /* Adjust as needed */
}
/* Adjust spacing around LatexEditor itself slightly */
.markdown-content :deep(.latex-editor-container) {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
/* Make inline latex blend better */
.markdown-content :deep(.latex-editor-container .latex-inline) {
display: inline-block;
vertical-align: baseline; /* Align with text */
margin: 0 0.15em; /* Minimal horizontal spacing */
padding: 0 !important; /* Remove padding for inline */
}
.markdown-content :deep(.latex-editor-container .latex-inline .katex) {
font-size: 1em; /* Match surrounding text size */
padding: 0 !important;
}
.markdown-content :deep(li > p),
.markdown-content :deep(blockquote > p) {
margin-bottom: 0;
}
.markdown-content :deep(ul),
.markdown-content :deep(ol) {
margin-bottom: 1rem;
padding-left: 2em;
}
.markdown-content :deep(blockquote) {
margin: 1em 0;
padding-left: 1em;
border-left: 3px solid #ccc;
color: #666;
}
.markdown-content :deep(hr) {
margin: 2em 0;
border: 0;
border-top: 1px solid #eee;
}
</style>

View File

@ -1,487 +0,0 @@
<template>
<div class="message group relative border-2 border-transparent hover:border-blue-400 dark:hover:border-sky-500 rounded-lg transition-colors duration-150 ease-in-out">
<div class="flex flex-row gap-2">
<div class="flex-shrink-0">
<div class="group/avatar">
<img :src="getImgUrl()" @error="defaultImg($event)" :data-popover-target="'avatar' + message.id" data-popover-placement="bottom"
class="w-10 h-10 rounded-full object-fill border border-blue-300 dark:border-slate-600">
</div>
</div>
<div class="flex flex-col w-full flex-grow">
<div class="flex flex-row flex-grow items-start">
<div class="flex flex-col mb-2">
<div class="message-header">{{ message.sender }}</div>
<div class="text-xs text-blue-500 dark:text-slate-400 font-thin" v-if="message.created_at"
:title="'Created at: ' + created_at_parsed">
{{ created_at }}
</div>
</div>
<div class="flex-grow"></div>
</div>
<div class="message-content overflow-x-auto w-full overflow-y-auto scrollbar space-y-2">
<MarkdownRenderer
ref="mdRender"
v-if="!editMsgMode"
:host="host"
v-model:markdown-text="message.content"
:message_id="message.id"
:discussion_id="message.discussion_id"
:client_id="this.$store.state.client_id"
>
</MarkdownRenderer>
<div v-if="editMsgMode" class="w-full">
<MarkdownEditor
ref="markdownEditor"
v-model="editableContent"
:theme="editorTheme"
editor-class="min-h-[150px] max-h-[70vh] message-editor-content"
toolbar-class="md-editor-toolbar-theme"
button-class="md-editor-button-theme"
:toolbar-button-icon-size="16"
/>
</div>
<div v-if="message.metadata !== null && !editMsgMode">
<div v-for="(metadata, index) in (message.metadata?.filter(m => m?.title && m?.content) || [])" :key="'json-' + message.id + '-' + index" class="mt-2">
<JsonViewer :title="metadata.title" :data="metadata.content" :key="'msgjson-' + message.id" />
</div>
</div>
<DynamicUIRenderer v-if="message.ui && !editMsgMode" ref="ui" class="w-full mt-2" :ui="message.ui" :key="'msgui-' + message.id + '-' + ui_componentKey" />
<audio controls v-if="audio_url!=null && !editMsgMode" class="w-full mt-2" :key="audio_url">
<source :src="audio_url" type="audio/wav" ref="audio_player">
Your browser does not support the audio element.
</audio>
<div class="message-details w-full max-w-4xl mx-auto mt-2">
<div v-if="message.steps && message.steps.length > 0 && !editMsgMode" class="steps-container">
<div class="steps-header" @click="toggleExpanded">
<div class="w-5 h-5 mr-2 flex-shrink-0 flex items-center justify-center">
<transition name="fade-icon" mode="out-in">
<div v-if="isProcessingSteps" key="header-spinner" class="step-spinner"></div>
<svg v-else-if="finalStepsStatus === true" key="header-success" class="step-icon-success w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>
<svg v-else-if="finalStepsStatus === false" key="header-fail" class="step-icon-fail w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>
<svg v-else key="header-unknown" class="w-4 h-4 text-gray-400 dark:text-slate-500" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
</transition>
</div>
<span class="steps-status truncate pr-2 text-sm">{{ headerStepText }}</span>
<span class="toggle-icon text-xs transform transition-transform duration-200 ml-auto" :class="{ 'rotate-180': expanded }">
<i data-feather="chevron-down" class="w-5 h-5"></i>
</span>
</div>
<transition
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-[500px]"
leave-from-class="opacity-100 max-h-[500px]"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="expanded" class="steps-content">
<div v-for="(step, index) in message.steps" :key="`step-${message.id}-${index}`" class="step-item animate-step-slide-in" :style="{ animationDelay: `${index * 80}ms` }">
<Step :done="step.done" :text="step.text" :status="step.status" :description="step.description"/>
</div>
</div>
</transition>
</div>
<div v-if="message.html_js_s && message.html_js_s.length && !editMsgMode" class="mt-2 flex flex-col items-start w-full overflow-y-auto scrollbar">
<div v-for="(html_js, index) in message.html_js_s" :key="`htmljs-${message.id}-${index}`" class="w-full animate-fadeIn" :style="{ animationDelay: `${index * 200}ms` }">
<RenderHTMLJS :htmlContent="html_js" />
</div>
</div>
</div>
</div>
<div class="message-toolbar-wrapper">
<div class="message-toolbar">
<div v-if="editMsgMode" class="flex items-center gap-1">
<ToolbarButton @click.stop="cancelEdit" title="Cancel edit" icon="x" class="svg-button text-red-500 hover:bg-red-100 dark:hover:bg-red-900" />
<ToolbarButton @click.stop="updateMessage" title="Update message" icon="check" class="svg-button text-green-500 hover:bg-green-100 dark:hover:bg-green-900" />
</div>
<div v-else class="flex items-center gap-1">
<ToolbarButton @click.stop="startEdit" title="Edit message" icon="edit" class="svg-button toolbar-button" />
<ToolbarButton @click="copyContentToClipboard" title="Copy message to clipboard" icon="copy" class="svg-button toolbar-button" />
<div v-if="message.sender !== $store.state.mountedPers.name" class="flex items-center gap-1">
<ToolbarButton @click.stop="resendMessage('full_context')" title="Resend message with full context" icon="send" class="svg-button toolbar-button" />
<ToolbarButton @click.stop="resendMessage('full_context_with_internet')" title="Resend message with internet search" icon="globe" class="svg-button toolbar-button" />
<ToolbarButton @click.stop="resendMessage('simple_question')" title="Resend message without context" icon="refresh-cw" class="svg-button toolbar-button" />
</div>
<div v-if="message.sender === $store.state.mountedPers.name" class="flex items-center gap-1">
<ToolbarButton @click.stop="continueMessage" title="Continue message" icon="fast-forward" class="svg-button toolbar-button" />
</div>
<div v-if="deleteMsgMode" class="flex items-center gap-1">
<ToolbarButton @click.stop="deleteMsgMode = false" title="Cancel removal" icon="x" class="svg-button toolbar-button text-blue-500 hover:bg-blue-100 dark:hover:bg-blue-700" />
<ToolbarButton @click.stop="deleteMsg()" title="Confirm removal" icon="check" class="svg-button text-red-500 hover:bg-red-100 dark:hover:bg-red-900" />
</div>
<ToolbarButton v-else title="Remove message" icon="trash" @click="deleteMsgMode = true" class="svg-button text-red-500 hover:bg-red-100 dark:hover:bg-red-900" />
<ToolbarButton @click.stop="rankUp()" title="Upvote" icon="thumbs-up" class="svg-button toolbar-button text-blue-500 dark:text-blue-400" />
<div class="flex items-center">
<ToolbarButton @click.stop="rankDown()" title="Downvote" icon="thumbs-down" class="svg-button text-red-500 dark:text-red-400" />
<div v-if="message.rank != 0" class="text-xs font-bold rounded-full px-1.5 py-0.5 flex items-center justify-center cursor-default" :class="message.rank > 0 ? 'bg-blue-500 text-white' : 'bg-red-500 text-white'" title="Rank">{{ message.rank }}</div>
</div>
<div v-if="this.$store.state.config.active_tts_service!='None'" class="flex items-center gap-1">
<ToolbarButton title="Speak message" icon="volume-2" @click.stop="speak()" class="svg-button toolbar-button" :class="{ 'text-red-500 dark:text-red-400 animate-pulse': isSpeaking }"/>
</div>
<div v-if="this.$store.state.config.xtts_enable && !this.$store.state.config.xtts_use_streaming_mode" class="flex items-center gap-1">
<ToolbarButton v-if="!isSynthesizingVoice" title="Generate audio" icon="mic" @click.stop="read()" class="svg-button toolbar-button" />
<img v-else :src="loading_svg" class="w-5 h-5 animate-spin text-blue-500 dark:text-sky-400">
</div>
</div>
</div>
</div>
<div class="message-footer">
<div class="flex flex-row flex-wrap items-center gap-x-3 gap-y-1">
<p v-if="message.binding" class="footer-item">Binding: <span class="footer-value">{{ message.binding }}</span></p>
<p v-if="message.model" class="footer-item">Model: <span class="footer-value">{{ message.model }}</span></p>
<p v-if="message.seed" class="footer-item">Seed: <span class="footer-value">{{ message.seed }}</span></p>
<p v-if="message.nb_tokens" class="footer-item">Tokens: <span class="footer-value">{{ message.nb_tokens }}</span></p>
<p v-if="warmup_duration" class="footer-item">Warmup: <span class="footer-value">{{ warmup_duration }}</span></p>
<p v-if="time_spent" class="footer-item">Gen time: <span class="footer-value">{{ time_spent }}</span></p>
<p v-if="generation_rate" class="footer-item">Rate: <span class="footer-value">{{ generation_rate }}</span></p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import botImgPlaceholder from "../assets/logo.png"
import { nextTick, computed } from 'vue'
import feather from 'feather-icons'
import DynamicUIRenderer from "./DynamicUIRenderer.vue"
import MarkdownRenderer from './MarkdownRenderer.vue';
import RenderHTMLJS from './RenderHTMLJS.vue';
import JsonViewer from "./JsonViewer.vue";
import Step from './Step.vue';
import StatusIcon from './StatusIcon.vue';
import axios from 'axios';
import loading_svg from '@/assets/loading.svg';
import ToolbarButton from './ToolbarButton.vue';
import DropdownMenu from './DropdownMenu.vue';
import DropdownSubmenu from './DropdownSubmenu.vue';
import MarkdownEditor from './MarkdownEditor.vue';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
export default {
name: 'Message',
emits: ['delete', 'rankUp', 'rankDown', 'updateMessage', 'resendMessage', 'continueMessage'],
components: {
MarkdownRenderer,
Step,
StatusIcon,
RenderHTMLJS,
JsonViewer,
DynamicUIRenderer,
ToolbarButton,
DropdownMenu,
DropdownSubmenu,
MarkdownEditor,
},
props: {
host: { type: String, required: false, default: "http://localhost:9600" },
message: Object,
avatar: { default: '' }
},
data() {
return {
ui_componentKey: 0,
isSynthesizingVoice: false,
loading_svg: loading_svg,
audio_url: null,
isSpeaking: false,
speechSynthesis: null,
voices: [],
expanded: false,
editMsgMode_: false,
originalContentBeforeEdit: '',
editableContent: '',
deleteMsgMode: false,
}
},
mounted() {
if ('speechSynthesis' in window) {
this.speechSynthesis = window.speechSynthesis;
this.loadVoices();
this.speechSynthesis.onvoiceschanged = this.loadVoices;
} else {
console.error('Speech synthesis is not supported in this browser.');
}
this.syncAudioUrlFromMetadata();
nextTick(feather.replace);
},
methods: {
loadVoices() {
if (!this.speechSynthesis) return;
this.voices = this.speechSynthesis.getVoices();
},
syncAudioUrlFromMetadata() {
if (Array.isArray(this.message.metadata)) {
const audioEntry = this.message.metadata.find(m => m?.audio_url);
this.audio_url = audioEntry ? audioEntry.audio_url : null;
} else {
this.audio_url = null;
}
},
toggleExpanded() {
this.expanded = !this.expanded;
nextTick(feather.replace);
},
computeTimeDiff(startTime, endTime){
let timeDiff = endTime.getTime() - startTime.getTime();
const hours = Math.floor(timeDiff / 3600000);
timeDiff -= hours * 3600000;
const mins = Math.floor(timeDiff / 60000);
timeDiff -= mins * 60000;
const secs = Math.floor(timeDiff / 1000);
return [hours, mins, secs];
},
read(){
if(this.isSynthesizingVoice){
this.isSynthesizingVoice=false;
if (this.$refs.audio_player) {
this.$refs.audio_player.pause();
}
} else {
this.isSynthesizingVoice=true;
axios.post(`${this.host}/text2wav`,{text:this.message.content}).then(response => {
this.isSynthesizingVoice=false;
const newAudioUrl = response.data.url;
this.audio_url = newAudioUrl;
if(!Array.isArray(this.message.metadata)) this.message.metadata = [];
let audioEntry = this.message.metadata.find(m => m && typeof m === 'object' && m.hasOwnProperty('audio_url'));
if (audioEntry) audioEntry.audio_url = newAudioUrl;
else this.message.metadata.push({audio_url: newAudioUrl});
nextTick(()=>{
if (this.$refs.audio_player) {
this.$refs.audio_player.load();
this.$refs.audio_player.play().catch(e => console.error("Audio autoplay failed:", e));
}
});
}).catch(ex=>{
this.$store.state.toast.showToast(`Error generating audio: ${ex.message || ex}`,4,false);
this.isSynthesizingVoice=false;
});
}
},
async speak() {
if (this.isSpeaking) {
if (this.$store.state.config.active_tts_service !== "browser" && this.$store.state.config.active_tts_service !== "None") {
try {
await axios.post(`${this.host}/stop_audio`, { client_id: this.$store.state.client_id });
this.isSpeaking = false;
} catch(ex) {
this.$store.state.toast.showToast(`Error stopping audio: ${ex.message || ex}`, 4, false);
this.isSpeaking = false;
}
} else if (this.speechSynthesis) {
this.speechSynthesis.cancel();
this.isSpeaking = false;
}
return;
}
this.isSpeaking = true;
const contentToSpeak = this.message.content;
if (this.$store.state.config.active_tts_service !== "browser" && this.$store.state.config.active_tts_service !== "None") {
axios.post(`${this.host}/text2Audio`, { client_id: this.$store.state.client_id, text: contentToSpeak })
.catch(ex => {
this.$store.state.toast.showToast(`Error starting backend TTS: ${ex.message || ex}`, 4, false);
this.isSpeaking = false;
});
} else if (this.speechSynthesis && contentToSpeak) {
let startIndex = 0;
const chunkSize = 180;
const selectedVoice = this.voices.find(voice => voice.name === this.$store.state.config.audio_out_voice);
const findLastSentenceIndex = (startIdx) => {
let textChunk = contentToSpeak.substring(startIdx, startIdx + chunkSize);
const endOfSentenceMarkers = ['.', '!', '?', '\n', ';', ':'];
let lastIndex = -1;
endOfSentenceMarkers.forEach(marker => { lastIndex = Math.max(lastIndex, textChunk.lastIndexOf(marker)); });
if (lastIndex === -1) lastIndex = textChunk.length === chunkSize ? (textChunk.lastIndexOf(' ') > -1 ? textChunk.lastIndexOf(' ') : chunkSize - 1) : textChunk.length - 1;
return lastIndex + startIdx + 1;
};
const speakChunk = () => {
if (!this.isSpeaking || startIndex >= contentToSpeak.length) { this.isSpeaking = false; return; }
const endIndex = findLastSentenceIndex(startIndex);
const chunk = contentToSpeak.substring(startIndex, endIndex).trim();
startIndex = endIndex;
if (chunk) {
const msg = new SpeechSynthesisUtterance(chunk);
msg.pitch = this.$store.state.config.audio_pitch || 1;
msg.rate = this.$store.state.config.audio_rate || 1;
if (selectedVoice) msg.voice = selectedVoice;
msg.onend = () => setTimeout(speakChunk, 50);
msg.onerror = (event) => { console.error("Speech error:", event.error); this.isSpeaking = false; };
this.speechSynthesis.speak(msg);
} else if (startIndex < contentToSpeak.length) speakChunk();
else this.isSpeaking = false;
};
speakChunk();
} else this.isSpeaking = false;
},
copyContentToClipboard() {
navigator.clipboard.writeText(this.message.content).then(() => {
this.$store.state.toast.showToast("Message copied to clipboard!", 4, true);
}).catch(err => {
this.$store.state.toast.showToast("Failed to copy message: " + err, 4, false);
});
},
deleteMsg() {
this.$emit('delete', this.message.id);
this.deleteMsgMode = false;
},
rankUp() { this.$emit('rankUp', this.message.id); },
rankDown() { this.$emit('rankDown', this.message.id); },
startEdit() {
this.originalContentBeforeEdit = this.message.content;
this.editableContent = this.message.content;
this.editMsgMode = true;
nextTick(() => {
if(this.$refs.markdownEditor && this.$refs.markdownEditor.editorView) {
this.$refs.markdownEditor.editorView.focus();
}
});
},
cancelEdit() { this.editMsgMode = false; },
updateMessage() {
console.log(`sending updateMessage with: ${this.message.id}, ${this.editableContent}, ${this.message.metadata}`)
this.$emit('updateMessage', {id:this.message.id, content:this.editableContent, metadata:this.message.metadata});
this.editMsgMode = false;
},
resendMessage(msg_type) { this.$emit('resendMessage', this.message.id, this.message.content, msg_type); },
continueMessage() { this.$emit('continueMessage', this.message.id, this.message.content); },
getImgUrl() { return this.avatar || botImgPlaceholder; },
defaultImg(event) { event.target.src = botImgPlaceholder; },
prettyDate(time) {
if (!time) return "";
try {
const date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " "));
if (isNaN(date)) return time;
const diff = (((new Date()).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400);
if (day_diff < 0) return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
if (day_diff === 0) {
if (diff < 60) return "just now"; if (diff < 120) return "1 minute ago"; if (diff < 3600) return Math.floor(diff / 60) + " minutes ago";
if (diff < 7200) return "1 hour ago"; return Math.floor(diff / 3600) + " hours ago";
}
if (day_diff === 1) return "Yesterday"; if (day_diff < 7) return day_diff + " days ago";
if (day_diff < 31) return Math.ceil(day_diff / 7) + " weeks ago";
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
} catch(e) { return time; }
},
checkForFullSentence() {
const trimmedContent = this.message.content.trim(); const lastChar = trimmedContent.slice(-1);
const sentenceEnders = ['.', '!', '?', '\n'];
if (sentenceEnders.includes(lastChar) && trimmedContent.split(/\s+/).length > 2) this.speak();
},
},
watch: {
'message.open': {
handler(newValue) {
if (newValue === true && !this.editMsgMode_) this.startEdit();
else if (newValue === false && this.editMsgMode_) this.cancelEdit();
},
immediate: true
},
editMsgMode_(newVal) { nextTick(feather.replace); },
audio_url(newUrl) {
nextTick(()=>{ if (newUrl && this.$refs.audio_player) this.$refs.audio_player.load(); });
},
'message.content': function (newContent, oldContent) {
if (this.$store.state.config.auto_speak &&
!(this.$store.state.config.xtts_enable && this.$store.state.config.xtts_use_streaming_mode) &&
!this.isSpeaking && newContent !== oldContent && !this.editMsgMode_) {
this.checkForFullSentence();
}
if(!this.editMsgMode_) this.editableContent = newContent;
},
'message.ui': function (newUI, oldUI) {
if (JSON.stringify(newUI) !== JSON.stringify(oldUI)) this.ui_componentKey++;
},
'message.metadata': { handler() { this.syncAudioUrlFromMetadata(); }, deep: true },
deleteMsgMode() { nextTick(feather.replace); },
},
computed: {
editorTheme() {
const isDarkMode = this.$store.state.config.darkMode;
return isDarkMode ? oneDark : EditorView.baseTheme({});
},
editMsgMode:{
get(){ return this.editMsgMode_; },
set(value){
if (this.editMsgMode_ === value) return;
this.editMsgMode_ = value;
if (this.message && typeof this.message === 'object' && this.message.hasOwnProperty('open')) {
if (this.message.open !== value) this.message.open = value;
}
}
},
activeStepIndex() { return Array.isArray(this.message.steps) ? this.message.steps.findIndex(step => !step.done) : -1; },
isProcessingSteps() { return this.activeStepIndex !== -1; },
headerStepText() {
if (!Array.isArray(this.message.steps) || this.message.steps.length === 0) return this.message.status_message || "Processing Steps";
if (this.isProcessingSteps && this.message.steps[this.activeStepIndex]) return this.message.steps[this.activeStepIndex].text || "Processing...";
if (this.message.status_message && this.message.status_message !== 'Thinking...') return this.message.status_message;
return "Processing Complete";
},
finalStepsStatus() {
if (!Array.isArray(this.message.steps) || this.isProcessingSteps || this.message.steps.length === 0) return null;
const failedStep = this.message.steps.find(step => step.status === false);
if (failedStep) return false;
const lowerCaseStatus = (this.message.status_message || "").toLowerCase();
if (lowerCaseStatus.includes("error") || lowerCaseStatus.includes("fail")) return false;
return true;
},
created_at() { return this.prettyDate(this.message.created_at); },
created_at_parsed() { try { return new Date(Date.parse(this.message.created_at)).toLocaleString(); } catch (e) { return this.message.created_at; } },
finished_generating_at_parsed() { try { return new Date(Date.parse(this.message.finished_generating_at)).toLocaleString(); } catch (e) { return this.message.finished_generating_at; } },
time_spent() {
if (!this.message.started_generating_at || !this.message.finished_generating_at) return undefined;
try {
const startTime = new Date(Date.parse(this.message.started_generating_at)), endTime = new Date(Date.parse(this.message.finished_generating_at));
if (isNaN(startTime) || isNaN(endTime) || endTime <= startTime) return "0s";
let [h, m, s] = this.computeTimeDiff(startTime, endTime); const z = (n) => n < 10 ? "0" + n : n; let parts = [];
if (h > 0) parts.push(z(h) + "h"); if (m > 0) parts.push(z(m) + "m"); parts.push(z(s) + 's'); return parts.join(':');
} catch (e) { return undefined; }
},
warmup_duration() {
if (!this.message.created_at || !this.message.started_generating_at) return undefined;
try {
const createdTime = new Date(Date.parse(this.message.created_at)), startTime = new Date(Date.parse(this.message.started_generating_at));
if (isNaN(createdTime) || isNaN(startTime) || startTime <= createdTime) return "0s";
let [h, m, s] = this.computeTimeDiff(createdTime, startTime); const z = (n) => n < 10 ? "0" + n : n; let parts = [];
if (h > 0) parts.push(z(h) + "h"); if (m > 0) parts.push(z(m) + "m"); parts.push(z(s) + 's'); return parts.join(':');
} catch (e) { return undefined; }
},
generation_rate() {
if (!this.message.started_generating_at || !this.message.finished_generating_at || !this.message.nb_tokens || this.message.nb_tokens <= 0) return undefined;
try {
const startTime = new Date(Date.parse(this.message.started_generating_at)), endTime = new Date(Date.parse(this.message.finished_generating_at));
if (isNaN(startTime) || isNaN(endTime)) return undefined; const timeDiff = endTime.getTime() - startTime.getTime();
if (timeDiff <= 0) return undefined; const secs = timeDiff / 1000; return Math.round(this.message.nb_tokens / secs) + " t/s";
} catch(e) { return undefined; }
}
}
}
</script>
<style scoped>
.message { padding-bottom: 2.5rem; }
.message-details .steps-container .step-item:last-child { margin-bottom: 0; }
@keyframes step-slide-in { from { opacity: 0; transform: translateX(-15px); } to { opacity: 1; transform: translateX(0); } }
.animate-step-slide-in { animation: step-slide-in 0.35s ease-out forwards; }
.fade-icon-enter-active, .fade-icon-leave-active { transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; }
.fade-icon-enter-from, .fade-icon-leave-to { opacity: 0; transform: scale(0.8); }
.fade-icon-enter-to, .fade-icon-leave-from { opacity: 1; transform: scale(1); }
@keyframes spin { to { transform: rotate(360deg); } }
.svg-button i[data-feather] { width: 1.1rem; height: 1.1rem; }
:deep(.cm-editor) { font-size: 0.95rem; }
:deep(.cm-scroller) { font-family: 'Consolas', 'Monaco', 'Courier New', Courier, monospace; }
</style>

View File

@ -27,7 +27,7 @@
</template>
<script>
import MarkdownRenderer from './MarkdownRenderer.vue';
import MarkdownRenderer from './MarkdownBundle/MarkdownRenderer.vue';
export default {
data() {

View File

@ -1,14 +0,0 @@
<template>
<div class="w-full h-full overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary" v-html="htmlContent"></div>
</template>
<script>
export default {
props: {
htmlContent: {
type: String,
required: true
}
}
}
</script>

View File

@ -47,8 +47,8 @@
<script>
import axios from 'axios';
import Discussion from '../components/Discussion.vue'
import MarkdownRenderer from '../components/MarkdownRenderer.vue'
import Discussion from '../views/discussion_page_components/Discussion.vue'
import MarkdownRenderer from './MarkdownBundle/MarkdownRenderer.vue'
export default {

View File

@ -1,191 +0,0 @@
<!-- ThinkingBlock.vue -->
<template>
<div class="my-4 bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden border border-gray-200 dark:border-gray-700">
<!-- Header / Toggle Area -->
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
<button
@click="toggle"
:aria-expanded="isOpen"
:aria-controls="contentId"
class="group flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 dark:focus-visible:ring-offset-gray-800 rounded"
>
<!-- Chevron Icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
class="w-5 h-5 transition-transform duration-300 ease-in-out flex-shrink-0"
:class="{ 'rotate-90': isOpen }">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
</svg>
<!-- Thinking State / Title -->
<span v-if="isThinking" class="flex items-center gap-2">
<span>Thinking</span>
<span class="inline-flex items-center space-x-1">
<span v-for="i in 3" :key="i"
class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce"
:style="{ animationDelay: `${(i-1)*150}ms` }"></span>
</span>
</span>
<span v-else>AI Thoughts</span>
</button>
<!-- Download Button -->
<button
v-if="!isThinking && content"
@click="downloadMarkdown"
class="p-1.5 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-500 dark:focus-visible:ring-offset-gray-700/50 transition-colors duration-150"
title="Download as Markdown"
>
<span class="sr-only">Download AI Thoughts as Markdown</span>
<!-- Download Icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<path d="M10.75 2.75a.75.75 0 00-1.5 0v8.614L6.295 8.235a.75.75 0 10-1.09 1.03l4.25 4.5a.75.75 0 001.09 0l4.25-4.5a.75.75 0 00-1.09-1.03l-2.955 3.129V2.75z" />
<path d="M3.5 12.75a.75.75 0 00-1.5 0v2.5A2.75 2.75 0 004.75 18h10.5A2.75 2.75 0 0018 15.25v-2.5a.75.75 0 00-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5z" />
</svg>
</button>
</div>
<!-- Collapsible Content Area -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="transform -translate-y-2 scale-95 opacity-0"
enter-to-class="transform translate-y-0 scale-100 opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="transform translate-y-0 scale-100 opacity-100"
leave-to-class="transform -translate-y-2 scale-95 opacity-0"
>
<div v-show="isOpen" class="content-wrapper" :id="contentId">
<div
ref="contentContainer"
class="p-4 text-gray-700 dark:text-gray-300 thinking-prose prose-sm max-w-none overflow-y-auto max-h-[400px] bg-gray-50 dark:bg-gray-800/50"
>
<!-- Slot for potential custom rendering -->
<slot v-if="$slots.default"></slot>
<!-- Default Markdown Rendering -->
<div v-else v-html="renderedContent"></div>
<!-- Optional: Blinking cursor effect while thinking -->
<span v-if="isThinking" class="inline-block w-2 h-4 ml-1 bg-gray-600 dark:bg-gray-400 animate-pulse"></span>
</div>
</div>
</transition>
</div>
</template>
<script>
import { ref, computed, watch, nextTick, onMounted } from 'vue';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import hljs from 'highlight.js';
// Choose a theme for highlight.js - github-dark is a good option for dark mode support
import 'highlight.js/styles/github-dark.css'; // Or choose another theme like github.css
// Configure Marked
marked.setOptions({
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
try {
return hljs.highlight(code, { language, ignoreIllegals: true }).value;
} catch (error) {
console.error("Highlight.js error:", error);
return hljs.highlightAuto(code).value; // Fallback
}
},
breaks: true, // Render line breaks as <br>
gfm: true, // Enable GitHub Flavored Markdown
pedantic: false,
smartLists: true,
smartypants: false,
});
// Unique ID generation helper (simple)
let idCounter = 0;
export default {
name: 'ThinkingBlock',
props: {
content: {
type: String,
required: true,
},
isDone: {
type: Boolean,
required: true,
default: false,
},
startOpen: { // Optional: Prop to control initial state
type: Boolean,
default: false,
}
},
setup(props) {
const isOpen = ref(props.startOpen);
const contentContainer = ref(null);
const contentId = `thinking-content-${idCounter++}`; // Unique ID for aria-controls
const isThinking = computed(() => !props.isDone);
const renderedContent = computed(() => {
// Sanitize *after* Markdown parsing
const rawHtml = marked.parse(props.content || '');
// Allow specific target attributes if needed, e.g., for links
return DOMPurify.sanitize(rawHtml, { ADD_ATTR: ['target'] });
});
const toggle = () => {
isOpen.value = !isOpen.value;
if (isOpen.value) {
nextTick(scrollToBottom);
}
};
const scrollToBottom = () => {
if (contentContainer.value) {
contentContainer.value.scrollTop = contentContainer.value.scrollHeight;
}
};
const downloadMarkdown = () => {
const blob = new Blob([props.content], { type: 'text/markdown;charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'ai_thoughts.md');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
};
// Watch for content changes to scroll down, especially when open
watch(() => props.content, () => {
if (isOpen.value) {
nextTick(scrollToBottom);
}
});
// Watch for `isDone` changing
watch(() => props.isDone, (newValue) => {
// Ensure scroll happens if it finishes while open
if (newValue && isOpen.value) {
nextTick(scrollToBottom);
}
});
// Initial scroll if starting open
onMounted(() => {
if (isOpen.value) {
nextTick(scrollToBottom);
}
});
return {
isOpen,
isThinking,
renderedContent,
contentContainer,
contentId,
toggle,
downloadMarkdown,
};
}
}
</script>

View File

@ -210,6 +210,13 @@ export const store = createStore({
saveStarredToLocalStorage(state.starredPersonalities);
}
},
removeStarredDiscussion(state, personalityPath) {
const index = state.starredPersonalities.indexOf(personalityPath);
if (index > -1) {
state.starredPersonalities.splice(index, 1);
saveStarredToLocalStorage(state.starredPersonalities);
}
},
setDiskUsage(state, diskUsage) { state.diskUsage = diskUsage; },
setRamUsage(state, ramUsage) { state.ramUsage = ramUsage; },
setVramUsage(state, vramUsage) { state.vramUsage = vramUsage; },
@ -740,6 +747,22 @@ export const store = createStore({
// 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);

View File

@ -139,7 +139,7 @@ export default defineComponent({
progress_visibility: false,
progress_value: 0,
interestingFacts: [
"ParisNeo, the creator of LoLLMs, originally built his high-performance PC to play Cyberpunk 2077. However, his passion for AI took an unexpected turn, leading him to develop LoLLMs instead. Ironically, he never found the time to actually play the game that inspired his powerful setup!",
"ParisNeo, the creator of LoLLMs, originally built his high-performance PC to play Cyberpunk 2077. However, his passion for AI took an unexpected turn, leading him to develop LoLLMs instead. Ironically, he never found the time to actually play the game that inspired his powerful setup!",
"Saïph, version 14 of LoLLMs, is named after a star in Orion's constellation (Kappa Orionis), representing bright guidance in AI!",
"The 'LoLLMs' name stands for 'Lord of Large Language Models', a playful nod to the power and potential of these AI systems.",
"LoLLMs v15 introduced 'Personality Packages', allowing users to customize AI interactions like never before.",
@ -210,7 +210,7 @@ export default defineComponent({
},
},
methods: {
...mapActions(['refreshConfig', 'refreshDatabase', 'refreshBindings', 'refreshPersonalitiesZoo', 'refreshMountedPersonalities', 'refreshModelsZoo', 'refreshModels', 'fetchLanguages', 'fetchLanguage', 'fetchIsRtOn', 'toggleStarPersonality', 'applyConfiguration', 'saveConfiguration', 'refreshModelStatus']),
...mapActions(['refreshConfig', 'refreshDatabase', 'refreshBindings', 'refreshPersonalitiesZoo', 'refreshMountedPersonalities', 'refreshModelsZoo', 'refreshModels', 'fetchLanguages', 'fetchLanguage', 'fetchIsRtOn', 'toggleStarPersonality', 'toggleStarDiscussion', 'applyConfiguration', 'saveConfiguration', 'refreshModelStatus']),
async initialLoad() {
console.log("Initial Load Started");
@ -398,7 +398,7 @@ export default defineComponent({
},
toggleStarDiscussion(item) {
this.toggleStarPersonality(String(item.id));
this.toggleStarDiscussionString(item.id);
this.$nextTick(() => { this.$forceUpdate(); }); // May be needed for LeftPanel list re-render based on getter
},
@ -670,7 +670,7 @@ export default defineComponent({
resetDB(){ console.warn("Reset DB function not fully implemented."); this.$store.state.toast.showToast("Database reset functionality not available.", 4, false); },
showModelConfig(item = null) {
const bindingToShow = item || this.$store.state.bindings.find(b => b.name === this.config.binding_name);
const bindingToShow = item || this.$store.state.installedBindings.find(b => b.name === this.config.binding_name);
if (!bindingToShow) { this.$store.state.toast.showToast("No binding selected or found.", 4, false); return; }
try { this.loading = true; axios.post('/get_active_binding_settings', { client_id: this.client_id, binding_name: bindingToShow.name }) .then(res => { if (res.data && Object.keys(res.data).length > 0) { this.$store.state.universalForm.showForm(res.data, `Configure ${bindingToShow.name}`, "Save", "Cancel") .then(formData => { axios.post('/set_binding_settings', { client_id: this.client_id, binding_name: bindingToShow.name, settings: formData }).then(saveRes => { if (!saveRes.data?.status) throw new Error(saveRes.data?.error || "Save failed."); this.$store.state.toast.showToast(`${bindingToShow.name} settings updated.`, 4, true); }).catch(saveErr => this.$store.state.toast.showToast(`Error saving settings: ${saveErr.message}`, 5, false)); }) .catch(() => {}); } else { this.$store.state.toast.showToast(`${bindingToShow.name} has no configurable settings.`, 3, true); } }) .catch(err => this.$store.state.toast.showToast(`Error getting settings: ${err.message}`, 5, false)) .finally(() => this.loading = false); }
catch (error) { this.loading = false; this.$store.state.toast.showToast(`Error: ${error.message}`, 5, false); }

View File

@ -330,7 +330,7 @@ import feather from 'feather-icons'
import axios from "axios";
import socket from '@/services/websocket.js'
import Toast from '../components/Toast.vue'
import MarkdownRenderer from '../components/MarkdownRenderer.vue';
import MarkdownRenderer from '../components/MarkdownBundle/MarkdownRenderer.vue';
import ClipBoardTextInput from "@/components/ClipBoardTextInput.vue";
import TokensHilighter from "@/components/TokensHilighter.vue"
import ChatBarButton from "@/components/ChatBarButton.vue"