wip: sse implementation chat

This commit is contained in:
Per Stark committed 2025-02-24 12:23:58 +01:00
1 parent e43b63de9f
commit c41e370b81
16 files changed
+361 -63

No files matched your search

+1
View File
@@ -0,0 +1 @@
(function(){var g;htmx.defineExtension("sse",{init:function(e){g=e;if(htmx.createEventSource==undefined){htmx.createEventSource=t}},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(e,t){var r=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var n=g.getInternalData(r);var s=n.sseEventSource;if(s){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"nodeReplaced"});n.sseEventSource.close()}return;case"htmx:afterProcessNode":i(r)}}});function t(e){return new EventSource(e,{withCredentials:true})}function a(n){if(g.getAttributeValue(n,"sse-swap")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var t=g.getAttributeValue(n,"sse-swap");var r=t.split(",");for(var i=0;i<r.length;i++){const u=r[i].trim();const c=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(u,c);return}if(!g.triggerEvent(n,"htmx:sseBeforeMessage",e)){return}f(n,e.data);g.triggerEvent(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=c;a.addEventListener(u,c)}}if(g.getAttributeValue(n,"hx-trigger")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var o=g.getTriggerSpecs(n);o.forEach(function(t){if(t.trigger.slice(0,4)!=="sse:"){return}var r=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(t.trigger.slice(4),r)}htmx.trigger(n,t.trigger,e);htmx.trigger(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=r;a.addEventListener(t.trigger.slice(4),r)})}}function i(e,t){if(e==null){return null}if(g.getAttributeValue(e,"sse-connect")){var r=g.getAttributeValue(e,"sse-connect");if(r==null){return}n(e,r,t)}a(e)}function n(r,e,n){var s=htmx.createEventSource(e);s.onerror=function(e){g.triggerErrorEvent(r,"htmx:sseError",{error:e,source:s});if(l(r)){return}if(s.readyState===EventSource.CLOSED){n=n||0;n=Math.max(Math.min(n*2,128),1);var t=n*500;window.setTimeout(function(){i(r,n)},t)}};s.onopen=function(e){g.triggerEvent(r,"htmx:sseOpen",{source:s});if(n&&n>0){const t=r.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let e=0;e<t.length;e++){a(t[e])}n=0}};g.getInternalData(r).sseEventSource=s;var t=g.getAttributeValue(r,"sse-close");if(t){s.addEventListener(t,function(){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"message"});s.close()})}}function l(e){if(!g.bodyContains(e)){var t=g.getInternalData(e).sseEventSource;if(t!=undefined){g.triggerEvent(e,"htmx:sseClose",{source:t,type:"nodeMissing"});t.close();return true}}return false}function f(t,r){g.withExtensions(t,function(e){r=e.transformResponse(r,null,t)});var e=g.getSwapSpecification(t);var n=g.getTarget(t);g.swap(n,r,e)}function v(e){return g.getInternalData(e).sseEventSource!=null}})();
+1
View File
@@ -0,0 +1 @@
(function(){var g;htmx.defineExtension("sse",{init:function(e){g=e;if(htmx.createEventSource==undefined){htmx.createEventSource=t}},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(e,t){var r=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var n=g.getInternalData(r);var s=n.sseEventSource;if(s){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"nodeReplaced"});n.sseEventSource.close()}return;case"htmx:afterProcessNode":i(r)}}});function t(e){return new EventSource(e,{withCredentials:true})}function a(n){if(g.getAttributeValue(n,"sse-swap")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var t=g.getAttributeValue(n,"sse-swap");var r=t.split(",");for(var i=0;i<r.length;i++){const u=r[i].trim();const c=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(u,c);return}if(!g.triggerEvent(n,"htmx:sseBeforeMessage",e)){return}f(n,e.data);g.triggerEvent(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=c;a.addEventListener(u,c)}}if(g.getAttributeValue(n,"hx-trigger")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var o=g.getTriggerSpecs(n);o.forEach(function(t){if(t.trigger.slice(0,4)!=="sse:"){return}var r=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(t.trigger.slice(4),r)}htmx.trigger(n,t.trigger,e);htmx.trigger(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=r;a.addEventListener(t.trigger.slice(4),r)})}}function i(e,t){if(e==null){return null}if(g.getAttributeValue(e,"sse-connect")){var r=g.getAttributeValue(e,"sse-connect");if(r==null){return}n(e,r,t)}a(e)}function n(r,e,n){var s=htmx.createEventSource(e);s.onerror=function(e){g.triggerErrorEvent(r,"htmx:sseError",{error:e,source:s});if(l(r)){return}if(s.readyState===EventSource.CLOSED){n=n||0;n=Math.max(Math.min(n*2,128),1);var t=n*500;window.setTimeout(function(){i(r,n)},t)}};s.onopen=function(e){g.triggerEvent(r,"htmx:sseOpen",{source:s});if(n&&n>0){const t=r.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let e=0;e<t.length;e++){a(t[e])}n=0}};g.getInternalData(r).sseEventSource=s;var t=g.getAttributeValue(r,"sse-close");if(t){s.addEventListener(t,function(){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"message"});s.close()})}}function l(e){if(!g.bodyContains(e)){var t=g.getInternalData(e).sseEventSource;if(t!=undefined){g.triggerEvent(e,"htmx:sseClose",{source:t,type:"nodeMissing"});t.close();return true}}return false}function f(t,r){g.withExtensions(t,function(e){r=e.transformResponse(r,null,t)});var e=g.getSwapSpecification(t);var n=g.getTarget(t);g.swap(n,r,e)}function v(e){return g.getInternalData(e).sseEventSource!=null}})();
+1 -1
View File
@@ -1 +1 @@
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var z={onLoad:t,process:Tt,on:le,off:ue,trigger:ie,ajax:dr,find:b,findAll:f,closest:d,values:function(e,t){var r=Jt(e,t||"post");return r.values},remove:B,addClass:j,removeClass:n,toggleClass:U,takeClass:V,defineExtension:yr,removeExtension:br,logAll:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=z.config.wsBinaryType;return t},version:"1.9.2"};var C={addTriggerHandler:xt,bodyContains:ee,canAccessLocalStorage:D,filterValues:er,hasAttribute:q,getAttributeValue:G,getClosestMatch:c,getExpressionVars:fr,getHeaders:Qt,getInputValues:Jt,getInternalData:Y,getSwapSpecification:rr,getTriggerSpecs:ze,getTarget:de,makeFragment:l,mergeObjects:te,makeSettleInfo:S,oobSwap:me,selectAndSwap:Me,settleImmediately:Bt,shouldCancel:Ke,triggerEvent:ie,triggerErrorEvent:ne,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function $(e,t){return e.getAttribute&&e.getAttribute(t)}function q(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function G(e,t){return $(e,t)||$(e,"data-"+t)}function u(e){return e.parentElement}function J(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=G(t,r);var i=G(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Z(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function H(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=J().createDocumentFragment()}return i}function L(e){return e.match(/<body/)}function l(e){var t=!L(e);if(z.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=H(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function K(e){if(e){e()}}function A(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function N(e){return A(e,"Function")}function I(e){return A(e,"Object")}function Y(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function k(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function Q(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function P(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function ee(e){if(e.getRootNode&&e.getRootNode()instanceof ShadowRoot){return J().body.contains(e.getRootNode().host)}else{return J().body.contains(e)}}function M(e){return e.trim().split(/\s+/)}function te(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function D(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function X(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return sr(J().body,function(){return eval(e)})}function t(t){var e=z.Line truncated
var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toStrinLine truncated
+107
View File
@@ -2155,12 +2155,21 @@
.\!top-2\.5 {
top: calc(var(--spacing) * 2.5) !important;
}
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-2 {
top: calc(var(--spacing) * 2);
}
.top-5 {
top: calc(var(--spacing) * 5);
}
.top-8 {
top: calc(var(--spacing) * 8);
}
.top-9 {
top: calc(var(--spacing) * 9);
}
.top-10 {
top: calc(var(--spacing) * 10);
}
@@ -2538,6 +2547,12 @@
.z-50 {
z-index: 50;
}
.z-\[\12 ަ��\$���w�����T�\:��G��\1e \] {
z-index: ަ��$���w�����T�:��G��;
}
.z-\[\12 ަ��\$���ӕ\/\12 h�Jc�\1a \\���\=�i\] {
z-index: ަ��$���ӕ/h�Jc�\���=�i;
}
.modal-box {
grid-column-start: 1;
grid-row-start: 1;
@@ -2630,6 +2645,9 @@
max-width: 96rem;
}
}
.m-2 {
margin: calc(var(--spacing) * 2);
}
.m-9 {
margin: calc(var(--spacing) * 9);
}
@@ -4059,12 +4077,21 @@
.h-5 {
height: calc(var(--spacing) * 5);
}
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.h-24 {
height: calc(var(--spacing) * 24);
}
.h-32 {
height: calc(var(--spacing) * 32);
}
.h-36 {
height: calc(var(--spacing) * 36);
}
.min-h-\[100dvh\] {
min-height: 100dvh;
}
@@ -4074,6 +4101,15 @@
.min-h-screen {
min-height: 100vh;
}
.loading-sm {
width: calc(var(--size-selector, 0.25rem) * 5);
}
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-5 {
width: calc(var(--spacing) * 5);
}
@@ -4083,6 +4119,9 @@
.w-72 {
width: calc(var(--spacing) * 72);
}
.w-\[���\}�C�9JO\1 �j\] {
width: ���}�C�9JO�j;
}
.w-full {
width: 100%;
}
@@ -4283,6 +4322,9 @@
.overflow-x-auto {
overflow-x: auto;
}
.rounded-2xl {
border-radius: var(--radius-2xl);
}
.rounded-box {
border-radius: var(--radius-box);
}
@@ -4315,6 +4357,17 @@
border-style: var(--tw-border-style);
border-width: 2px;
}
.textarea-ghost {
background-color: transparent;
box-shadow: none;
border-color: transparent;
&:focus, &:focus-within {
background-color: var(--color-base-100);
color: var(--color-base-content);
border-color: transparent;
box-shadow: none;
}
}
.alert-success {
border-color: var(--color-success);
color: var(--color-success-content);
@@ -4348,6 +4401,12 @@
--tw-gradient-position: to right in oklab,;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.\!bg-none {
background-image: none !important;
}
.bg-none {
background-image: none;
}
.from-primary {
--tw-gradient-from: var(--color-primary);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position,) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -4380,6 +4439,9 @@
.p-5 {
padding: calc(var(--spacing) * 5);
}
.p-9 {
padding: calc(var(--spacing) * 9);
}
.menu-title {
padding-inline: calc(0.25rem * 3);
padding-block: calc(0.25rem * 2);
@@ -4416,15 +4478,30 @@
.py-4 {
padding-block: calc(var(--spacing) * 4);
}
.pt-5 {
padding-top: calc(var(--spacing) * 5);
}
.pt-10 {
padding-top: calc(var(--spacing) * 10);
}
.pr-3 {
padding-right: calc(var(--spacing) * 3);
}
.pr-8 {
padding-right: calc(var(--spacing) * 8);
}
.pr-12 {
padding-right: calc(var(--spacing) * 12);
}
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
}
.pb-4 {
padding-bottom: calc(var(--spacing) * 4);
}
.pb-8 {
padding-bottom: calc(var(--spacing) * 8);
}
.pb-10 {
padding-bottom: calc(var(--spacing) * 10);
}
@@ -4664,6 +4741,9 @@
--btn-color: var(--color-secondary);
--btn-fg: var(--color-secondary-content);
}
.loading-dots {
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='4' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1'/%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1' begin='0.1s'/%3E%3C/circle%3E%3Ccircle cx='20' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1' begin='0.2s'/%3E%3C/circle%3E%3C/svg%3E");
}
.prose {
:root & {
--tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, transparent);
@@ -4696,6 +4776,16 @@
margin-top: calc(var(--spacing) * 4);
}
}
.sm\:mb-2 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 2);
}
}
.sm\:mb-5 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 5);
}
}
.sm\:max-w-md {
@media (width >= 40rem) {
max-width: var(--container-md);
@@ -4711,11 +4801,28 @@
flex-direction: row;
}
}
.sm\:rounded-b-2xl {
@media (width >= 40rem) {
border-bottom-right-radius: var(--radius-2xl);
border-bottom-left-radius: var(--radius-2xl);
}
}
.sm\:rounded-b-none {
@media (width >= 40rem) {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
.sm\:px-0 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 0);
}
}
.sm\:pb-4 {
@media (width >= 40rem) {
padding-bottom: calc(var(--spacing) * 4);
}
}
.sm\:text-6xl {
@media (width >= 40rem) {
font-size: var(--text-6xl);
+109 -35
View File
@@ -1,18 +1,29 @@
use std::time::Duration;
use axum::{
extract::State,
response::{IntoResponse, Redirect},
extract::{Path, Query, State},
response::{
sse::{Event, KeepAlive},
Html, IntoResponse, Redirect, Sse,
},
Form,
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use futures::{stream, Stream, StreamExt};
use surrealdb::{engine::any::Any, Surreal};
use tokio::time::sleep;
use tracing::info;
use uuid::Uuid;
use crate::{
error::HtmlError,
page_data,
server::{routes::html::render_template, AppState},
storage::types::user::User,
storage::types::{
message::{Message, MessageRole},
user::User,
},
};
// Update your ChatStartParams struct to properly deserialize the references
@@ -36,27 +47,8 @@ where
page_data!(ChatData, "chat/base.html", {
user: User,
history: Vec<Message>,
});
#[derive(Deserialize, Debug, Serialize)]
pub enum MessageRole {
User,
AI,
System,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct Message {
conversation_id: String,
role: MessageRole,
content: String,
references: Option<Vec<String>>,
}
pub struct Conversation {
user_id: String,
title: String,
}
});
pub async fn show_initialized_chat(
State(state): State<AppState>,
@@ -72,19 +64,16 @@ pub async fn show_initialized_chat(
info!("{:?}", form);
let user_message = Message {
conversation_id: "test".to_string(),
role: MessageRole::User,
content: form.user_query,
references: None,
};
let conversation_id = Uuid::new_v4().to_string();
let ai_message = Message {
conversation_id: "test".to_string(),
role: MessageRole::AI,
content: form.llm_response,
references: Some(form.references),
};
let user_message = Message::new("test".to_string(), MessageRole::User, form.user_query, None);
let ai_message = Message::new(
"test".to_string(),
MessageRole::AI,
form.llm_response,
Some(form.references),
);
let messages = vec![user_message, ai_message];
@@ -93,6 +82,7 @@ pub async fn show_initialized_chat(
ChatData {
history: messages,
user,
conversation_id,
},
state.templates.clone(),
)?;
@@ -111,14 +101,98 @@ pub async fn show_chat_base(
None => return Ok(Redirect::to("/").into_response()),
};
let conversation_id = Uuid::new_v4().to_string();
let output = render_template(
ChatData::template_name(),
ChatData {
history: vec![],
user,
conversation_id,
},
state.templates.clone(),
)?;
Ok(output.into_response())
}
#[derive(Deserialize)]
pub struct NewMessageForm {
content: String,
}
pub async fn new_user_message(
Path(conversation_id): Path<String>,
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<NewMessageForm>,
) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying empty chat start");
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let query_id = Uuid::new_v4().to_string();
let user_message = form.content.clone();
// Save to database
// state
// .db
// .save(conversation_id, query_id.clone(), user_message)
// .await;
#[derive(Serialize)]
struct SSEResponseInitData {
user_message: String,
query_id: String,
}
let output = render_template(
"chat/streaming_response.html",
SSEResponseInitData {
user_message,
query_id,
},
state.templates.clone(),
)?;
Ok(output.into_response())
}
#[derive(Deserialize)]
pub struct QueryParams {
query_id: String,
}
pub async fn get_response_stream(
State(_state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Query(params): Query<QueryParams>,
) -> Sse<impl Stream<Item = Result<Event, axum::Error>>> {
let stream = stream::iter(vec![
Event::default()
.event("chat_message")
.data("Hello, starting stream!"),
Event::default()
.event("chat_message")
.data("This is message 2"),
Event::default().event("chat_message").data("Final message"),
Event::default()
.event("close_stream")
.data("Stream complete"), // Signal to close
])
.then(|event| async move {
sleep(Duration::from_millis(500)).await; // Delay between messages
Ok(event)
});
info!("Streaming started");
Sse::new(stream).keep_alive(
axum::response::sse::KeepAlive::new()
.interval(Duration::from_secs(15))
.text("keep-alive"),
)
}
+3 -1
View File
@@ -16,7 +16,7 @@ use axum_session_surreal::SessionSurrealPool;
use html::{
account::{delete_account, set_api_key, show_account_page, update_timezone},
admin_panel::{show_admin_panel, toggle_registration_status},
chat::{show_chat_base, show_initialized_chat},
chat::{get_response_stream, new_user_message, show_chat_base, show_initialized_chat},
content::{patch_text_content, show_content_page, show_text_content_edit_form},
documentation::{
show_documentation_index, show_get_started, show_mobile_friendly, show_privacy_policy,
@@ -65,6 +65,8 @@ pub fn html_routes(app_state: &AppState) -> Router<AppState> {
.route("/gdpr/deny", post(deny_gdpr))
.route("/search", get(search_result_handler))
.route("/chat", get(show_chat_base).post(show_initialized_chat))
.route("/chat/:id", post(new_user_message))
.route("/chat/response-stream", get(get_response_stream))
.route("/signout", get(sign_out_user))
.route("/signin", get(show_signin_form).post(authenticate_user))
.route(
+21
View File
@@ -0,0 +1,21 @@
use uuid::Uuid;
use crate::stored_object;
stored_object!(Conversation, "conversation", {
user_id: String,
title: String
});
impl Conversation {
pub fn new(user_id: String, title: String) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
user_id,
title,
}
}
}
+37
View File
@@ -0,0 +1,37 @@
use uuid::Uuid;
use crate::stored_object;
#[derive(Deserialize, Debug, Serialize)]
pub enum MessageRole {
User,
AI,
System,
}
stored_object!(Message, "message", {
conversation_id: String,
role: MessageRole,
content: String,
references: Option<Vec<String>>
});
impl Message {
pub fn new(
conversation_id: String,
role: MessageRole,
content: String,
references: Option<Vec<String>>,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
conversation_id,
role,
content,
references,
}
}
}
+2
View File
@@ -1,10 +1,12 @@
use axum::async_trait;
use serde::{Deserialize, Serialize};
pub mod analytics;
pub mod conversation;
pub mod file_info;
pub mod job;
pub mod knowledge_entity;
pub mod knowledge_relationship;
pub mod message;
pub mod system_settings;
pub mod text_chunk;
pub mod text_content;
+1 -3
View File
@@ -9,9 +9,7 @@
<div class="relative w-full">
{% include "chat/history.html" %}
<div class="fixed w-full mx-auto max-w-3xl left-0 right-0 bottom-0">
{% include "chat/input_field.html" %}
</div>
{% include "chat/new_message_form.html" %}
</div>
</main>
</div>
+21 -1
View File
@@ -16,4 +16,24 @@
</div>
{% endif %}
{% endfor %}
</div>
</div>
<script>
// Scroll to latest message after HTMX swap
document.body.addEventListener('htmx:afterSwap', function (evt) {
const chatContainer = document.getElementById('chat_container');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
});
</script>
<style>
#chat_container {
max-height: 70vh;
/* Adjust as needed */
overflow-y: auto;
/* Enable scrolling */
padding: 1rem;
}
</style>
-22
View File
@@ -1,22 +0,0 @@
<form hx-post="/chat/{{conversation_id}}" hx-target="#chat_container" hx-swap="afterend" class="relative flex gap-2"
id="chat-form">
<textarea name="content" placeholder="Type your message..." rows="2"
class="textarea rounded-t-2xl rounded-b-none border-2 flex-grow resize-none" id="chat-input"></textarea>
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-2">{% include
"icons/send_icon.html" %}
</button>
<label for="my-drawer-2" class="absolute cursor-pointer top-10 right-0.5 p-2 drawer-button xl:hidden z-20 ">
{% include "icons/hamburger_icon.html" %}
</label>
</form>
<script>
document.getElementById('chat-input').addEventListener('keydown', function (e) {
// Check if Enter is pressed without Shift
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevent default Enter behavior (new line)
document.getElementById('chat-form').submit(); // Submit the form
}
// Shift + Enter will naturally create a new line due to browser default behavior
});
</script>
+29
View File
@@ -0,0 +1,29 @@
<div class="fixed w-full mx-auto max-w-3xl p-4 pb-0 sm:pb-4 left-0 right-0 bottom-0">
<form hx-post="/chat/{{conversation_id}}" hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2"
id="chat-form">
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
class="textarea textarea-ghost rounded-2xl rounded-b-none h-24 sm:rounded-b-2xl pr-8 bg-base-200 flex-grow resize-none"
id="chat-input"></textarea>
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-1">{% include
"icons/send_icon.html" %}
</button>
<label for="my-drawer-2" class="absolute cursor-pointer top-9 right-0.5 p-2 drawer-button xl:hidden z-20 ">
{% include "icons/hamburger_icon.html" %}
</label>
</form>
</div>
<script>
document.getElementById('chat-input').addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
htmx.trigger('#chat-form', 'submit');
}
});
// Clear textarea after successful submission
document.getElementById('chat-form').addEventListener('htmx:afterRequest', function (e) {
if (e.detail.successful) { // Check if the request was successful
document.getElementById('chat-input').value = ''; // Clear the textarea
}
});
</script>
+25
View File
@@ -0,0 +1,25 @@
<div class="chat chat-end">
<div class="chat-header">User</div>
<div class="chat-bubble">
{{user_message}}
</div>
</div>
<div class="chat chat-start">
<div class="chat-header">AI</div>
<div class="chat-bubble" hx-ext="sse" sse-connect="/chat/response-stream?query_id={{query_id}}"
sse-swap="chat_message" sse-close="close_stream" hx-swap="beforeend">
<span class="loading loading-dots loading-sm loading-id-{{query_id}}"></span>
</div>
</div>
<script>
document.body.addEventListener('htmx:sseBeforeMessage', (e) => {
const targetElement = e.detail.elt;
const loadingSpinner = targetElement.querySelector('.loading-id-{{query_id}}');
// Hiding the loading spinner before data is swapped in
if (loadingSpinner) {
loadingSpinner.style.display = 'none';
}
}
)
</script>
+2
View File
@@ -10,6 +10,7 @@
<!-- Preload critical assets -->
<link rel="preload" href="/assets/htmx.min.js" as="script">
<link rel="preload" href="/assets/htmx-ext-sse.js" as="script">
<link rel="preload" href="/assets/style.css" as="style">
<!-- Core styles -->
@@ -17,6 +18,7 @@
<!-- Scripts -->
<script src="/assets/htmx.min.js" defer></script>
<script src="/assets/htmx-ext-sse.js" defer></script>
<script src="/assets/theme-toggle.js" defer></script>
<!-- Icons -->
@@ -1,6 +1,7 @@
<div class="flex gap-4 flex-col sm:flex-row">
<a class="btn btn-secondary" href="/knowledge" hx-boost="true">View Knowledge</a>
<a class="btn btn-accent" href="/content" hx-boost="true">View Content</a>
<a class="btn btn-accent" href="/chat" hx-boost="true">Chat</a>
<button class="btn btn-primary" hx-get="/ingress-form" hx-target="#modal" hx-swap="innerHTML">Add
Content</button>
</div>