mirror of
https://github.com/perstarkse/minne.git
synced 2026-03-25 10:52:07 +01:00
refactor: async-stream and improved reference handling
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -6041,6 +6041,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-openai",
|
||||
"async-stream",
|
||||
"axum",
|
||||
"axum-htmx",
|
||||
"axum_session",
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
anyhow = "1.0.94"
|
||||
async-openai = "0.24.1"
|
||||
async-stream = "0.3.6"
|
||||
axum = { version = "0.7.5", features = ["multipart", "macros"] }
|
||||
axum-htmx = "0.6.0"
|
||||
axum_session = "0.14.4"
|
||||
|
||||
255
assets/style.css
255
assets/style.css
@@ -2057,6 +2057,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.indicator {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: max-content;
|
||||
:where(.indicator-item) {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
top: var(--inidicator-t, 0);
|
||||
bottom: var(--inidicator-b, auto);
|
||||
left: var(--inidicator-s, auto);
|
||||
right: var(--inidicator-e, 0);
|
||||
translate: var(--inidicator-x, 50%) var(--indicator-y, -50%);
|
||||
}
|
||||
}
|
||||
.collapse-title {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
@@ -2158,21 +2173,9 @@
|
||||
.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);
|
||||
}
|
||||
.right-0 {
|
||||
right: calc(var(--spacing) * 0);
|
||||
}
|
||||
@@ -2541,33 +2544,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.z-1 {
|
||||
z-index: 1;
|
||||
}
|
||||
.z-2 {
|
||||
z-index: 2;
|
||||
}
|
||||
.z-3 {
|
||||
z-index: 3;
|
||||
}
|
||||
.z-5 {
|
||||
z-index: 5;
|
||||
}
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
.z-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 {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
@@ -2639,9 +2621,6 @@
|
||||
.list-col-wrap {
|
||||
grid-row-start: 2;
|
||||
}
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
@@ -2660,12 +2639,6 @@
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.m-2 {
|
||||
margin: calc(var(--spacing) * 2);
|
||||
}
|
||||
.m-9 {
|
||||
margin: calc(var(--spacing) * 9);
|
||||
}
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -2783,6 +2756,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.my-0 {
|
||||
margin-block: calc(var(--spacing) * 0);
|
||||
}
|
||||
.my-2 {
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
.my-4 {
|
||||
margin-block: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -3679,6 +3658,9 @@
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -3719,6 +3701,9 @@
|
||||
.mb-10 {
|
||||
margin-bottom: calc(var(--spacing) * 10);
|
||||
}
|
||||
.ml-1 {
|
||||
margin-left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.alert {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@@ -3814,20 +3799,6 @@
|
||||
background-image: radial-gradient( circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), transparent );
|
||||
box-shadow: 0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), transparent);
|
||||
}
|
||||
.status\! {
|
||||
display: inline-block !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
width: calc(0.25rem * 2) !important;
|
||||
height: calc(0.25rem * 2) !important;
|
||||
border-radius: var(--radius-selector) !important;
|
||||
background-color: color-mix(in oklab, var(--color-base-content) 20%, transparent) !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
vertical-align: middle !important;
|
||||
color: color-mix(in oklab, var(--color-black) 30%, transparent) !important;
|
||||
background-image: radial-gradient( circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), transparent ) !important;
|
||||
box-shadow: 0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), transparent) !important;
|
||||
}
|
||||
.kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -4089,27 +4060,18 @@
|
||||
width: calc(var(--spacing) * 10);
|
||||
height: calc(var(--spacing) * 10);
|
||||
}
|
||||
.h-4 {
|
||||
height: calc(var(--spacing) * 4);
|
||||
.h-3 {
|
||||
height: calc(var(--spacing) * 3);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
@@ -4122,11 +4084,8 @@
|
||||
.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-3 {
|
||||
width: calc(var(--spacing) * 3);
|
||||
}
|
||||
.w-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
@@ -4137,9 +4096,6 @@
|
||||
.w-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 {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -4152,6 +4108,18 @@
|
||||
.max-w-3xl {
|
||||
max-width: var(--container-3xl);
|
||||
}
|
||||
.max-w-\[10ch\] {
|
||||
max-width: 10ch;
|
||||
}
|
||||
.max-w-\[20ch\] {
|
||||
max-width: 20ch;
|
||||
}
|
||||
.max-w-\[50px\] {
|
||||
max-width: 50px;
|
||||
}
|
||||
.max-w-\[90\%\] {
|
||||
max-width: 90%;
|
||||
}
|
||||
.max-w-\[160px\] {
|
||||
max-width: 160px;
|
||||
}
|
||||
@@ -4188,6 +4156,9 @@
|
||||
--tw-scale-z: 75%;
|
||||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
||||
}
|
||||
.rotate-180 {
|
||||
rotate: 180deg;
|
||||
}
|
||||
.swap-rotate {
|
||||
.swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on {
|
||||
rotate: 45deg;
|
||||
@@ -4225,6 +4196,9 @@
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: -50%;
|
||||
}
|
||||
.animate-pulse {
|
||||
animation: var(--animate-pulse);
|
||||
}
|
||||
.link {
|
||||
cursor: pointer;
|
||||
text-decoration-line: underline;
|
||||
@@ -4278,6 +4252,9 @@
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.gap-1 {
|
||||
gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-2 {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -4337,6 +4314,9 @@
|
||||
.overflow-clip {
|
||||
overflow: clip;
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
.overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -4355,9 +4335,8 @@
|
||||
.rounded-lg {
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.rounded-t-2xl {
|
||||
border-top-left-radius: var(--radius-2xl);
|
||||
border-top-right-radius: var(--radius-2xl);
|
||||
.rounded-md {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.rounded-t-none {
|
||||
border-top-left-radius: 0;
|
||||
@@ -4371,10 +4350,6 @@
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-2 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 2px;
|
||||
}
|
||||
.textarea-ghost {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
@@ -4391,14 +4366,17 @@
|
||||
color: var(--color-success-content);
|
||||
--alert-color: var(--color-success);
|
||||
}
|
||||
.border-base-300 {
|
||||
border-color: var(--color-base-300);
|
||||
}
|
||||
.border-base-content {
|
||||
border-color: var(--color-base-content);
|
||||
}
|
||||
.border-base-content\/5 {
|
||||
border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent);
|
||||
}
|
||||
.border-primary {
|
||||
border-color: var(--color-primary);
|
||||
.border-gray-200 {
|
||||
border-color: var(--color-gray-200);
|
||||
}
|
||||
.bg-accent {
|
||||
background-color: var(--color-accent);
|
||||
@@ -4409,6 +4387,12 @@
|
||||
.bg-base-200 {
|
||||
background-color: var(--color-base-200);
|
||||
}
|
||||
.bg-gray-50 {
|
||||
background-color: var(--color-gray-50);
|
||||
}
|
||||
.bg-gray-200 {
|
||||
background-color: var(--color-gray-200);
|
||||
}
|
||||
.bg-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
@@ -4419,12 +4403,6 @@
|
||||
--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));
|
||||
@@ -4451,18 +4429,12 @@
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
.p-3 {
|
||||
padding: calc(var(--spacing) * 3);
|
||||
}
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
.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);
|
||||
@@ -4470,6 +4442,11 @@
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-sm {
|
||||
--size: calc(var(--size-selector, 0.25rem) * 5);
|
||||
font-size: 0.75rem;
|
||||
padding-inline: calc(0.25rem * 2.5 - var(--border));
|
||||
}
|
||||
.badge-xs {
|
||||
--size: calc(var(--size-selector, 0.25rem) * 4);
|
||||
font-size: 0.625rem;
|
||||
@@ -4493,14 +4470,23 @@
|
||||
.px-1 {
|
||||
padding-inline: calc(var(--spacing) * 1);
|
||||
}
|
||||
.px-2 {
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
.px-4 {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.py-0 {
|
||||
padding-block: calc(var(--spacing) * 0);
|
||||
}
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-4 {
|
||||
padding-block: calc(var(--spacing) * 4);
|
||||
}
|
||||
.pt-5 {
|
||||
padding-top: calc(var(--spacing) * 5);
|
||||
.pt-1 {
|
||||
padding-top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.pt-10 {
|
||||
padding-top: calc(var(--spacing) * 10);
|
||||
@@ -4517,18 +4503,15 @@
|
||||
.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);
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.font-satoshi {
|
||||
font-family: Satoshi, sans-serif;
|
||||
}
|
||||
@@ -4586,6 +4569,10 @@
|
||||
--tw-font-weight: var(--font-weight-light);
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
.font-medium {
|
||||
--tw-font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.font-semibold {
|
||||
--tw-font-weight: var(--font-weight-semibold);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
@@ -4603,6 +4590,10 @@
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.badge-neutral {
|
||||
--badge-color: var(--color-neutral);
|
||||
color: var(--color-neutral-content);
|
||||
}
|
||||
.badge-primary {
|
||||
--badge-color: var(--color-primary);
|
||||
color: var(--color-primary-content);
|
||||
@@ -4625,12 +4616,21 @@
|
||||
.text-base-content\/60 {
|
||||
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
|
||||
}
|
||||
.text-blue-500 {
|
||||
color: var(--color-blue-500);
|
||||
}
|
||||
.text-error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
.text-gray-400 {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
.text-gray-500 {
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
.text-gray-700 {
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
.text-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
@@ -4672,6 +4672,9 @@
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.opacity-50 {
|
||||
opacity: 50%;
|
||||
}
|
||||
.opacity-60 {
|
||||
opacity: 60%;
|
||||
}
|
||||
@@ -4722,6 +4725,11 @@
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.transition-transform {
|
||||
transition-property: transform, translate, scale, rotate;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.ease-in {
|
||||
--tw-ease: var(--ease-in);
|
||||
transition-timing-function: var(--ease-in);
|
||||
@@ -4734,9 +4742,6 @@
|
||||
--tw-ease: var(--ease-out);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
.\[a-zA-Z\:\\-\] {
|
||||
a-zA-Z: \-;
|
||||
}
|
||||
.btn-accent {
|
||||
--btn-color: var(--color-accent);
|
||||
--btn-fg: var(--color-accent-content);
|
||||
@@ -4785,6 +4790,13 @@
|
||||
--tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, transparent);
|
||||
}
|
||||
}
|
||||
.hover\:text-blue-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:text-gray-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@@ -4792,21 +4804,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:underline {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.focus\:outline-none {
|
||||
&:focus {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
}
|
||||
}
|
||||
.sm\:mt-4 {
|
||||
@media (width >= 40rem) {
|
||||
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);
|
||||
@@ -4828,12 +4843,6 @@
|
||||
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);
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
use std::{pin::Pin, time::Duration};
|
||||
use std::{pin::Pin, sync::Arc, time::Duration};
|
||||
|
||||
use async_stream::stream;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
response::{sse::Event, Sse},
|
||||
response::{
|
||||
sse::{Event, KeepAlive},
|
||||
Sse,
|
||||
},
|
||||
};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use futures::{stream, Stream, StreamExt, TryStreamExt};
|
||||
use futures::{
|
||||
stream::{self, once},
|
||||
Stream, StreamExt, TryStreamExt,
|
||||
};
|
||||
use json_stream_parser::JsonStreamParser;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::from_str;
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
use tokio::sync::{mpsc::channel, Mutex};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
@@ -19,7 +28,7 @@ use crate::{
|
||||
create_chat_request, create_user_message, format_entities_json, LLMResponseFormat,
|
||||
},
|
||||
},
|
||||
server::AppState,
|
||||
server::{routes::html::render_template, AppState},
|
||||
storage::{
|
||||
db::{get_item, store_item, SurrealDbClient},
|
||||
types::{
|
||||
@@ -29,6 +38,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
// Error handling function
|
||||
fn create_error_stream(
|
||||
message: impl Into<String>,
|
||||
) -> Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>> {
|
||||
@@ -127,8 +137,9 @@ pub async fn get_response_stream(
|
||||
};
|
||||
|
||||
// 5. Create channel for collecting complete response
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(1000);
|
||||
let (tx, mut rx) = channel::<String>(1000);
|
||||
let tx_clone = tx.clone();
|
||||
let (tx_final, mut rx_final) = channel::<Vec<String>>(1);
|
||||
|
||||
// 6. Set up the collection task for DB storage
|
||||
let db_client = state.surreal_db_client.clone();
|
||||
@@ -142,13 +153,15 @@ pub async fn get_response_stream(
|
||||
}
|
||||
|
||||
// Try to extract structured data
|
||||
if let Ok(response) = serde_json::from_str::<LLMResponseFormat>(&full_json) {
|
||||
if let Ok(response) = from_str::<LLMResponseFormat>(&full_json) {
|
||||
let references: Vec<String> = response
|
||||
.references
|
||||
.into_iter()
|
||||
.map(|r| r.reference)
|
||||
.collect();
|
||||
|
||||
let _ = tx_final.send(references.clone()).await;
|
||||
|
||||
let ai_message = Message::new(
|
||||
user_message.conversation_id,
|
||||
MessageRole::AI,
|
||||
@@ -176,7 +189,7 @@ pub async fn get_response_stream(
|
||||
});
|
||||
|
||||
// Create a shared state for tracking the JSON parsing
|
||||
let json_state = std::sync::Arc::new(tokio::sync::Mutex::new(StreamParserState::new()));
|
||||
let json_state = Arc::new(Mutex::new(StreamParserState::new()));
|
||||
|
||||
// 7. Create the response event stream
|
||||
let event_stream = openai_stream
|
||||
@@ -185,7 +198,7 @@ pub async fn get_response_stream(
|
||||
let tx_storage = tx_clone.clone();
|
||||
let json_state = json_state.clone();
|
||||
|
||||
async move {
|
||||
stream! {
|
||||
match result {
|
||||
Ok(response) => {
|
||||
let content = response
|
||||
@@ -200,29 +213,71 @@ pub async fn get_response_stream(
|
||||
|
||||
// Process through JSON parser
|
||||
let mut state = json_state.lock().await;
|
||||
|
||||
let display_content = state.process_chunk(&content);
|
||||
drop(state);
|
||||
if !display_content.is_empty() {
|
||||
return Ok(Event::default()
|
||||
yield Ok(Event::default()
|
||||
.event("chat_message")
|
||||
.data(display_content));
|
||||
}
|
||||
|
||||
// Empty or filtered content
|
||||
Ok(Event::default().event("chat_message").data(""))
|
||||
} else {
|
||||
Ok(Event::default().event("chat_message").data(""))
|
||||
// If display_content is empty, don't yield anything
|
||||
}
|
||||
// If content is empty, don't yield anything
|
||||
}
|
||||
Err(e) => {
|
||||
yield Ok(Event::default()
|
||||
.event("error")
|
||||
.data(format!("Stream error: {}", e)));
|
||||
}
|
||||
Err(e) => Ok(Event::default()
|
||||
.event("error")
|
||||
.data(format!("Stream error: {}", e))),
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffered(10)
|
||||
.chain(stream::once(async {
|
||||
.flatten()
|
||||
.chain(stream::once(async move {
|
||||
if let Some(references) = rx_final.recv().await {
|
||||
// Don't send any event if references is empty
|
||||
if references.is_empty() {
|
||||
return Ok(Event::default().event("empty")); // This event won't be sent
|
||||
}
|
||||
|
||||
// Prepare data for template
|
||||
#[derive(Serialize)]
|
||||
struct ReferenceData {
|
||||
references: Vec<String>,
|
||||
user_message_id: String,
|
||||
}
|
||||
|
||||
// Render template with references
|
||||
match render_template(
|
||||
"chat/reference_list.html",
|
||||
ReferenceData {
|
||||
references,
|
||||
user_message_id: user_message.id,
|
||||
},
|
||||
state.templates.clone(),
|
||||
) {
|
||||
Ok(html) => {
|
||||
// Extract the String from Html<String>
|
||||
let html_string = html.0; // Convert Html<String> to String
|
||||
|
||||
// Return the rendered HTML
|
||||
Ok(Event::default().event("references").data(html_string))
|
||||
}
|
||||
Err(_) => {
|
||||
// Handle template rendering error
|
||||
Ok(Event::default()
|
||||
.event("error")
|
||||
.data("Failed to render references"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle case where no references were received
|
||||
Ok(Event::default()
|
||||
.event("error")
|
||||
.data("Failed to retrieve references"))
|
||||
}
|
||||
}))
|
||||
.chain(once(async {
|
||||
Ok(Event::default()
|
||||
.event("close_stream")
|
||||
.data("Stream complete"))
|
||||
@@ -230,7 +285,7 @@ pub async fn get_response_stream(
|
||||
|
||||
info!("OpenAI streaming started");
|
||||
Sse::new(event_stream.boxed()).keep_alive(
|
||||
axum::response::sse::KeepAlive::new()
|
||||
KeepAlive::new()
|
||||
.interval(Duration::from_secs(15))
|
||||
.text("keep-alive"),
|
||||
)
|
||||
@@ -259,7 +314,6 @@ impl StreamParserState {
|
||||
}
|
||||
|
||||
// Get the current state of the JSON
|
||||
// The get_result() method returns a &Value, not a Result
|
||||
let json = self.parser.get_result();
|
||||
|
||||
// Check if we're in the answer field
|
||||
@@ -283,46 +337,3 @@ impl StreamParserState {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Create the response event stream
|
||||
// let event_stream = openai_stream
|
||||
// .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
// .map(move |result| {
|
||||
// let tx = tx_clone.clone();
|
||||
// async move {
|
||||
// match result {
|
||||
// Ok(response) => {
|
||||
// let content = response
|
||||
// .choices
|
||||
// .first()
|
||||
// .and_then(|choice| choice.delta.content.clone())
|
||||
// .unwrap_or_default();
|
||||
|
||||
// if !content.is_empty() {
|
||||
// let _ = tx.send(content.clone()).await;
|
||||
// Ok(Event::default().event("chat_message").data(content))
|
||||
// } else {
|
||||
// Ok(Event::default().event("chat_message").data(""))
|
||||
// }
|
||||
// }
|
||||
// Err(e) => Ok(Event::default()
|
||||
// .event("error")
|
||||
// .data(format!("Stream error: {}", e))),
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .buffered(10)
|
||||
// .chain(stream::once(async {
|
||||
// Ok(Event::default()
|
||||
// .event("close_stream")
|
||||
// .data("Stream complete"))
|
||||
// }));
|
||||
|
||||
// info!("OpenAI streaming started");
|
||||
|
||||
// Sse::new(event_stream.boxed()).keep_alive(
|
||||
// axum::response::sse::KeepAlive::new()
|
||||
// .interval(Duration::from_secs(15))
|
||||
// .text("keep-alive"),
|
||||
// )
|
||||
// }
|
||||
|
||||
30
templates/chat/reference_list.html
Normal file
30
templates/chat/reference_list.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="relative my-2">
|
||||
<button id="references-toggle-{{user_message_id}}"
|
||||
class="text-xs text-blue-500 hover:text-blue-700 hover:underline focus:outline-none flex items-center">
|
||||
References
|
||||
{% include "icons/chevron_icon.html" %}
|
||||
</button>
|
||||
<div id="references-content-{{user_message_id}}" class="hidden max-w-full mt-1">
|
||||
<div class="flex flex-wrap">
|
||||
{% for reference in references %}
|
||||
<span class="badge badge-sm text-xs truncate max-w-[20ch] overflow-hidden text-left block tooltip"
|
||||
hx-get="/knowledge/{{reference}}" hx-trigger="mouseenter once delay:500ms"
|
||||
hx-target="#tooltip-content-{{loop.index}}-{{user_message_id}}" hx-swap="innerHTML">
|
||||
{{reference}}
|
||||
<div id="tooltip-content-{{loop.index}}-{{user_message_id}}" class="tooltip-content">
|
||||
<!-- Loading indicator while content is fetched -->
|
||||
<div class="animate-pulse text-gray-400 text-xs">Loading...</div>
|
||||
</div>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('references-toggle-{{user_message_id}}').addEventListener('click', function () {
|
||||
const content = document.getElementById('references-content-{{user_message_id}}');
|
||||
const icon = document.getElementById('toggle-icon');
|
||||
content.classList.toggle('hidden');
|
||||
icon.classList.toggle('rotate-180');
|
||||
});
|
||||
</script>
|
||||
@@ -1,14 +1,16 @@
|
||||
<div class="chat chat-end">
|
||||
<div class="chat-header">User</div>
|
||||
<div class="chat-bubble">
|
||||
{{user_message.content}}
|
||||
</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?message_id={{user_message.id}}"
|
||||
sse-swap="chat_message" sse-close="close_stream" hx-swap="beforeend">
|
||||
<span class="loading loading-dots loading-sm loading-id-{{user_message.id}}"></span>
|
||||
<div hx-ext="sse" sse-connect="/chat/response-stream?message_id={{user_message.id}}" sse-close="close_stream"
|
||||
hx-swap="beforeend">
|
||||
<div class="chat-bubble" sse-swap="chat_message">
|
||||
<span class="loading loading-dots loading-sm loading-id-{{user_message.id}}"></span>
|
||||
</div>
|
||||
<div class="chat-footer opacity-50 max-w-[90%] flex-wrap" sse-swap="references">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
6
templates/icons/chevron_icon.html
Normal file
6
templates/icons/chevron_icon.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg class="w-3 h-3 ml-1 transition-transform" id="toggle-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 326 B |
Reference in New Issue
Block a user