mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-25 02:08:30 +02:00
wip: sse implementation chat
This commit is contained in:
1
assets/htmx-ext-sse.js
Normal file
1
assets/htmx-ext-sse.js
Normal 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
assets/htmx-sse.min.js
vendored
Normal file
1
assets/htmx-sse.min.js
vendored
Normal 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}})();
|
||||||
2
assets/htmx.min.js
vendored
2
assets/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
107
assets/style.css
107
assets/style.css
@@ -2155,12 +2155,21 @@
|
|||||||
.\!top-2\.5 {
|
.\!top-2\.5 {
|
||||||
top: calc(var(--spacing) * 2.5) !important;
|
top: calc(var(--spacing) * 2.5) !important;
|
||||||
}
|
}
|
||||||
|
.top-1 {
|
||||||
|
top: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.top-2 {
|
.top-2 {
|
||||||
top: calc(var(--spacing) * 2);
|
top: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.top-5 {
|
.top-5 {
|
||||||
top: calc(var(--spacing) * 5);
|
top: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.top-8 {
|
||||||
|
top: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.top-9 {
|
||||||
|
top: calc(var(--spacing) * 9);
|
||||||
|
}
|
||||||
.top-10 {
|
.top-10 {
|
||||||
top: calc(var(--spacing) * 10);
|
top: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
@@ -2538,6 +2547,12 @@
|
|||||||
.z-50 {
|
.z-50 {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
.z-\[\12 ަ<EFBFBD><EFBFBD>\$<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>\:<EFBFBD><EFBFBD>G<EFBFBD><EFBFBD>\1e \] {
|
||||||
|
z-index: ަ<EFBFBD><EFBFBD>$<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>:<EFBFBD><EFBFBD>G<EFBFBD><EFBFBD>;
|
||||||
|
}
|
||||||
|
.z-\[\12 ަ<EFBFBD><EFBFBD>\$<EFBFBD><EFBFBD><EFBFBD>ӕ\/\12 h<EFBFBD>Jc<EFBFBD>\1a \\<EFBFBD><EFBFBD><EFBFBD>\=<EFBFBD>i\] {
|
||||||
|
z-index: ަ<EFBFBD><EFBFBD>$<EFBFBD><EFBFBD><EFBFBD>ӕ/h<EFBFBD>Jc<EFBFBD>\<EFBFBD><EFBFBD><EFBFBD>=<EFBFBD>i;
|
||||||
|
}
|
||||||
.modal-box {
|
.modal-box {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
grid-row-start: 1;
|
grid-row-start: 1;
|
||||||
@@ -2630,6 +2645,9 @@
|
|||||||
max-width: 96rem;
|
max-width: 96rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.m-2 {
|
||||||
|
margin: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.m-9 {
|
.m-9 {
|
||||||
margin: calc(var(--spacing) * 9);
|
margin: calc(var(--spacing) * 9);
|
||||||
}
|
}
|
||||||
@@ -4059,12 +4077,21 @@
|
|||||||
.h-5 {
|
.h-5 {
|
||||||
height: calc(var(--spacing) * 5);
|
height: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.h-6 {
|
||||||
|
height: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.h-9 {
|
.h-9 {
|
||||||
height: calc(var(--spacing) * 9);
|
height: calc(var(--spacing) * 9);
|
||||||
}
|
}
|
||||||
|
.h-24 {
|
||||||
|
height: calc(var(--spacing) * 24);
|
||||||
|
}
|
||||||
.h-32 {
|
.h-32 {
|
||||||
height: calc(var(--spacing) * 32);
|
height: calc(var(--spacing) * 32);
|
||||||
}
|
}
|
||||||
|
.h-36 {
|
||||||
|
height: calc(var(--spacing) * 36);
|
||||||
|
}
|
||||||
.min-h-\[100dvh\] {
|
.min-h-\[100dvh\] {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
@@ -4074,6 +4101,15 @@
|
|||||||
.min-h-screen {
|
.min-h-screen {
|
||||||
min-height: 100vh;
|
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 {
|
.w-5 {
|
||||||
width: calc(var(--spacing) * 5);
|
width: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
@@ -4083,6 +4119,9 @@
|
|||||||
.w-72 {
|
.w-72 {
|
||||||
width: calc(var(--spacing) * 72);
|
width: calc(var(--spacing) * 72);
|
||||||
}
|
}
|
||||||
|
.w-\[<EFBFBD><EFBFBD><EFBFBD>\}<EFBFBD>C<EFBFBD>9JO\1 <EFBFBD>j\] {
|
||||||
|
width: <EFBFBD><EFBFBD><EFBFBD>}<EFBFBD>C<EFBFBD>9JO<EFBFBD>j;
|
||||||
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -4283,6 +4322,9 @@
|
|||||||
.overflow-x-auto {
|
.overflow-x-auto {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
.rounded-2xl {
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
}
|
||||||
.rounded-box {
|
.rounded-box {
|
||||||
border-radius: var(--radius-box);
|
border-radius: var(--radius-box);
|
||||||
}
|
}
|
||||||
@@ -4315,6 +4357,17 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 2px;
|
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 {
|
.alert-success {
|
||||||
border-color: var(--color-success);
|
border-color: var(--color-success);
|
||||||
color: var(--color-success-content);
|
color: var(--color-success-content);
|
||||||
@@ -4348,6 +4401,12 @@
|
|||||||
--tw-gradient-position: to right in oklab,;
|
--tw-gradient-position: to right in oklab,;
|
||||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||||
}
|
}
|
||||||
|
.\!bg-none {
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
.bg-none {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
.from-primary {
|
.from-primary {
|
||||||
--tw-gradient-from: var(--color-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));
|
--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 {
|
.p-5 {
|
||||||
padding: calc(var(--spacing) * 5);
|
padding: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.p-9 {
|
||||||
|
padding: calc(var(--spacing) * 9);
|
||||||
|
}
|
||||||
.menu-title {
|
.menu-title {
|
||||||
padding-inline: calc(0.25rem * 3);
|
padding-inline: calc(0.25rem * 3);
|
||||||
padding-block: calc(0.25rem * 2);
|
padding-block: calc(0.25rem * 2);
|
||||||
@@ -4416,15 +4478,30 @@
|
|||||||
.py-4 {
|
.py-4 {
|
||||||
padding-block: calc(var(--spacing) * 4);
|
padding-block: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.pt-5 {
|
||||||
|
padding-top: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
.pt-10 {
|
.pt-10 {
|
||||||
padding-top: calc(var(--spacing) * 10);
|
padding-top: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
.pr-3 {
|
.pr-3 {
|
||||||
padding-right: calc(var(--spacing) * 3);
|
padding-right: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
|
.pr-8 {
|
||||||
|
padding-right: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.pr-12 {
|
.pr-12 {
|
||||||
padding-right: calc(var(--spacing) * 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 {
|
.pb-10 {
|
||||||
padding-bottom: calc(var(--spacing) * 10);
|
padding-bottom: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
@@ -4664,6 +4741,9 @@
|
|||||||
--btn-color: var(--color-secondary);
|
--btn-color: var(--color-secondary);
|
||||||
--btn-fg: var(--color-secondary-content);
|
--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 {
|
.prose {
|
||||||
:root & {
|
:root & {
|
||||||
--tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, transparent);
|
--tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, transparent);
|
||||||
@@ -4696,6 +4776,16 @@
|
|||||||
margin-top: calc(var(--spacing) * 4);
|
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 {
|
.sm\:max-w-md {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
max-width: var(--container-md);
|
max-width: var(--container-md);
|
||||||
@@ -4711,11 +4801,28 @@
|
|||||||
flex-direction: row;
|
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 {
|
.sm\:px-0 {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
padding-inline: calc(var(--spacing) * 0);
|
padding-inline: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sm\:pb-4 {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
padding-bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
.sm\:text-6xl {
|
.sm\:text-6xl {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
font-size: var(--text-6xl);
|
font-size: var(--text-6xl);
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::{Path, Query, State},
|
||||||
response::{IntoResponse, Redirect},
|
response::{
|
||||||
|
sse::{Event, KeepAlive},
|
||||||
|
Html, IntoResponse, Redirect, Sse,
|
||||||
|
},
|
||||||
Form,
|
Form,
|
||||||
};
|
};
|
||||||
use axum_session_auth::AuthSession;
|
use axum_session_auth::AuthSession;
|
||||||
use axum_session_surreal::SessionSurrealPool;
|
use axum_session_surreal::SessionSurrealPool;
|
||||||
|
use futures::{stream, Stream, StreamExt};
|
||||||
use surrealdb::{engine::any::Any, Surreal};
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
|
use tokio::time::sleep;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::HtmlError,
|
error::HtmlError,
|
||||||
page_data,
|
page_data,
|
||||||
server::{routes::html::render_template, AppState},
|
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
|
// Update your ChatStartParams struct to properly deserialize the references
|
||||||
@@ -36,27 +47,8 @@ where
|
|||||||
page_data!(ChatData, "chat/base.html", {
|
page_data!(ChatData, "chat/base.html", {
|
||||||
user: User,
|
user: User,
|
||||||
history: Vec<Message>,
|
history: Vec<Message>,
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Serialize)]
|
|
||||||
pub enum MessageRole {
|
|
||||||
User,
|
|
||||||
AI,
|
|
||||||
System,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Serialize)]
|
|
||||||
pub struct Message {
|
|
||||||
conversation_id: String,
|
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(
|
pub async fn show_initialized_chat(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -72,19 +64,16 @@ pub async fn show_initialized_chat(
|
|||||||
|
|
||||||
info!("{:?}", form);
|
info!("{:?}", form);
|
||||||
|
|
||||||
let user_message = Message {
|
let conversation_id = Uuid::new_v4().to_string();
|
||||||
conversation_id: "test".to_string(),
|
|
||||||
role: MessageRole::User,
|
|
||||||
content: form.user_query,
|
|
||||||
references: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let ai_message = Message {
|
let user_message = Message::new("test".to_string(), MessageRole::User, form.user_query, None);
|
||||||
conversation_id: "test".to_string(),
|
|
||||||
role: MessageRole::AI,
|
let ai_message = Message::new(
|
||||||
content: form.llm_response,
|
"test".to_string(),
|
||||||
references: Some(form.references),
|
MessageRole::AI,
|
||||||
};
|
form.llm_response,
|
||||||
|
Some(form.references),
|
||||||
|
);
|
||||||
|
|
||||||
let messages = vec![user_message, ai_message];
|
let messages = vec![user_message, ai_message];
|
||||||
|
|
||||||
@@ -93,6 +82,7 @@ pub async fn show_initialized_chat(
|
|||||||
ChatData {
|
ChatData {
|
||||||
history: messages,
|
history: messages,
|
||||||
user,
|
user,
|
||||||
|
conversation_id,
|
||||||
},
|
},
|
||||||
state.templates.clone(),
|
state.templates.clone(),
|
||||||
)?;
|
)?;
|
||||||
@@ -111,14 +101,98 @@ pub async fn show_chat_base(
|
|||||||
None => return Ok(Redirect::to("/").into_response()),
|
None => return Ok(Redirect::to("/").into_response()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let conversation_id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
let output = render_template(
|
let output = render_template(
|
||||||
ChatData::template_name(),
|
ChatData::template_name(),
|
||||||
ChatData {
|
ChatData {
|
||||||
history: vec![],
|
history: vec![],
|
||||||
user,
|
user,
|
||||||
|
conversation_id,
|
||||||
},
|
},
|
||||||
state.templates.clone(),
|
state.templates.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(output.into_response())
|
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"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use axum_session_surreal::SessionSurrealPool;
|
|||||||
use html::{
|
use html::{
|
||||||
account::{delete_account, set_api_key, show_account_page, update_timezone},
|
account::{delete_account, set_api_key, show_account_page, update_timezone},
|
||||||
admin_panel::{show_admin_panel, toggle_registration_status},
|
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},
|
content::{patch_text_content, show_content_page, show_text_content_edit_form},
|
||||||
documentation::{
|
documentation::{
|
||||||
show_documentation_index, show_get_started, show_mobile_friendly, show_privacy_policy,
|
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("/gdpr/deny", post(deny_gdpr))
|
||||||
.route("/search", get(search_result_handler))
|
.route("/search", get(search_result_handler))
|
||||||
.route("/chat", get(show_chat_base).post(show_initialized_chat))
|
.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("/signout", get(sign_out_user))
|
||||||
.route("/signin", get(show_signin_form).post(authenticate_user))
|
.route("/signin", get(show_signin_form).post(authenticate_user))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
21
src/storage/types/conversation.rs
Normal file
21
src/storage/types/conversation.rs
Normal 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
src/storage/types/message.rs
Normal file
37
src/storage/types/message.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
|
pub mod conversation;
|
||||||
pub mod file_info;
|
pub mod file_info;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
pub mod knowledge_entity;
|
pub mod knowledge_entity;
|
||||||
pub mod knowledge_relationship;
|
pub mod knowledge_relationship;
|
||||||
|
pub mod message;
|
||||||
pub mod system_settings;
|
pub mod system_settings;
|
||||||
pub mod text_chunk;
|
pub mod text_chunk;
|
||||||
pub mod text_content;
|
pub mod text_content;
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
{% include "chat/history.html" %}
|
{% include "chat/history.html" %}
|
||||||
|
|
||||||
<div class="fixed w-full mx-auto max-w-3xl left-0 right-0 bottom-0">
|
{% include "chat/new_message_form.html" %}
|
||||||
{% include "chat/input_field.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,4 +16,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% 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>
|
||||||
@@ -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
templates/chat/new_message_form.html
Normal file
29
templates/chat/new_message_form.html
Normal 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
templates/chat/streaming_response.html
Normal file
25
templates/chat/streaming_response.html
Normal 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>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
<!-- Preload critical assets -->
|
<!-- Preload critical assets -->
|
||||||
<link rel="preload" href="/assets/htmx.min.js" as="script">
|
<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">
|
<link rel="preload" href="/assets/style.css" as="style">
|
||||||
|
|
||||||
<!-- Core styles -->
|
<!-- Core styles -->
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/assets/htmx.min.js" defer></script>
|
<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>
|
<script src="/assets/theme-toggle.js" defer></script>
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Icons -->
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<div class="flex gap-4 flex-col sm:flex-row">
|
<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-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="/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
|
<button class="btn btn-primary" hx-get="/ingress-form" hx-target="#modal" hx-swap="innerHTML">Add
|
||||||
Content</button>
|
Content</button>
|
||||||
</div>
|
</div>
|
||||||
Reference in New Issue
Block a user