refactor: async-stream and improved reference handling

This commit is contained in:
Per Stark
2025-02-27 13:49:45 +01:00
parent 4ce272d5be
commit 21f0ebef33
7 changed files with 254 additions and 194 deletions
Generated
+1
View File
@@ -6041,6 +6041,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-openai", "async-openai",
"async-stream",
"axum", "axum",
"axum-htmx", "axum-htmx",
"axum_session", "axum_session",
+1
View File
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.94" anyhow = "1.0.94"
async-openai = "0.24.1" async-openai = "0.24.1"
async-stream = "0.3.6"
axum = { version = "0.7.5", features = ["multipart", "macros"] } axum = { version = "0.7.5", features = ["multipart", "macros"] }
axum-htmx = "0.6.0" axum-htmx = "0.6.0"
axum_session = "0.14.4" axum_session = "0.14.4"
+132 -123
View File
@@ -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 { .collapse-title {
grid-column-start: 1; grid-column-start: 1;
grid-row-start: 1; grid-row-start: 1;
@@ -2158,21 +2173,9 @@
.top-1 { .top-1 {
top: calc(var(--spacing) * 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-9 {
top: calc(var(--spacing) * 9); top: calc(var(--spacing) * 9);
} }
.top-10 {
top: calc(var(--spacing) * 10);
}
.right-0 { .right-0 {
right: calc(var(--spacing) * 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-20 {
z-index: 20; z-index: 20;
} }
.z-50 { .z-50 {
z-index: 50; z-index: 50;
} }
.z-\[\12 ަ\$wT\:G\1e \] {
z-index: ަ$wT:G;
}
.z-\[\12 ަ\$ӕ\/\12 hJc\1a \\\=i\] {
z-index: ަ$ӕ/hJc\=i;
}
.modal-box { .modal-box {
grid-column-start: 1; grid-column-start: 1;
grid-row-start: 1; grid-row-start: 1;
@@ -2639,9 +2621,6 @@
.list-col-wrap { .list-col-wrap {
grid-row-start: 2; grid-row-start: 2;
} }
.float-left {
float: left;
}
.container { .container {
width: 100%; width: 100%;
@media (width >= 40rem) { @media (width >= 40rem) {
@@ -2660,12 +2639,6 @@
max-width: 96rem; max-width: 96rem;
} }
} }
.m-2 {
margin: calc(var(--spacing) * 2);
}
.m-9 {
margin: calc(var(--spacing) * 9);
}
.filter { .filter {
display: flex; display: flex;
flex-wrap: wrap; 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 { .my-4 {
margin-block: calc(var(--spacing) * 4); margin-block: calc(var(--spacing) * 4);
} }
@@ -3679,6 +3658,9 @@
font-size: var(--text-xs); font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height)); line-height: var(--tw-leading, var(--text-xs--line-height));
} }
.mt-1 {
margin-top: calc(var(--spacing) * 1);
}
.mt-2 { .mt-2 {
margin-top: calc(var(--spacing) * 2); margin-top: calc(var(--spacing) * 2);
} }
@@ -3719,6 +3701,9 @@
.mb-10 { .mb-10 {
margin-bottom: calc(var(--spacing) * 10); margin-bottom: calc(var(--spacing) * 10);
} }
.ml-1 {
margin-left: calc(var(--spacing) * 1);
}
.alert { .alert {
display: grid; display: grid;
width: 100%; width: 100%;
@@ -3814,20 +3799,6 @@
background-image: radial-gradient( circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), transparent ); 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); 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 { .kbd {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -4089,27 +4060,18 @@
width: calc(var(--spacing) * 10); width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10); height: calc(var(--spacing) * 10);
} }
.h-4 { .h-3 {
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 3);
} }
.h-5 { .h-5 {
height: calc(var(--spacing) * 5); height: calc(var(--spacing) * 5);
} }
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.h-24 { .h-24 {
height: calc(var(--spacing) * 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;
} }
@@ -4122,11 +4084,8 @@
.loading-sm { .loading-sm {
width: calc(var(--size-selector, 0.25rem) * 5); width: calc(var(--size-selector, 0.25rem) * 5);
} }
.w-1 { .w-3 {
width: calc(var(--spacing) * 1); width: calc(var(--spacing) * 3);
}
.w-2 {
width: calc(var(--spacing) * 2);
} }
.w-5 { .w-5 {
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
@@ -4137,9 +4096,6 @@
.w-72 { .w-72 {
width: calc(var(--spacing) * 72); width: calc(var(--spacing) * 72);
} }
.w-\[\}C9JO\1 j\] {
width: }C9JOj;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@@ -4152,6 +4108,18 @@
.max-w-3xl { .max-w-3xl {
max-width: var(--container-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-w-\[160px\] {
max-width: 160px; max-width: 160px;
} }
@@ -4188,6 +4156,9 @@
--tw-scale-z: 75%; --tw-scale-z: 75%;
scale: var(--tw-scale-x) var(--tw-scale-y); scale: var(--tw-scale-x) var(--tw-scale-y);
} }
.rotate-180 {
rotate: 180deg;
}
.swap-rotate { .swap-rotate {
.swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on { .swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on {
rotate: 45deg; rotate: 45deg;
@@ -4225,6 +4196,9 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position-x: -50%; background-position-x: -50%;
} }
.animate-pulse {
animation: var(--animate-pulse);
}
.link { .link {
cursor: pointer; cursor: pointer;
text-decoration-line: underline; text-decoration-line: underline;
@@ -4278,6 +4252,9 @@
.justify-start { .justify-start {
justify-content: flex-start; justify-content: flex-start;
} }
.gap-1 {
gap: calc(var(--spacing) * 1);
}
.gap-2 { .gap-2 {
gap: calc(var(--spacing) * 2); gap: calc(var(--spacing) * 2);
} }
@@ -4337,6 +4314,9 @@
.overflow-clip { .overflow-clip {
overflow: clip; overflow: clip;
} }
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto { .overflow-x-auto {
overflow-x: auto; overflow-x: auto;
} }
@@ -4355,9 +4335,8 @@
.rounded-lg { .rounded-lg {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
} }
.rounded-t-2xl { .rounded-md {
border-top-left-radius: var(--radius-2xl); border-radius: var(--radius-md);
border-top-right-radius: var(--radius-2xl);
} }
.rounded-t-none { .rounded-t-none {
border-top-left-radius: 0; border-top-left-radius: 0;
@@ -4371,10 +4350,6 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
} }
.border-2 {
border-style: var(--tw-border-style);
border-width: 2px;
}
.textarea-ghost { .textarea-ghost {
background-color: transparent; background-color: transparent;
box-shadow: none; box-shadow: none;
@@ -4391,14 +4366,17 @@
color: var(--color-success-content); color: var(--color-success-content);
--alert-color: var(--color-success); --alert-color: var(--color-success);
} }
.border-base-300 {
border-color: var(--color-base-300);
}
.border-base-content { .border-base-content {
border-color: var(--color-base-content); border-color: var(--color-base-content);
} }
.border-base-content\/5 { .border-base-content\/5 {
border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent); border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent);
} }
.border-primary { .border-gray-200 {
border-color: var(--color-primary); border-color: var(--color-gray-200);
} }
.bg-accent { .bg-accent {
background-color: var(--color-accent); background-color: var(--color-accent);
@@ -4409,6 +4387,12 @@
.bg-base-200 { .bg-base-200 {
background-color: var(--color-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 { .bg-secondary {
background-color: var(--color-secondary); background-color: var(--color-secondary);
} }
@@ -4419,12 +4403,6 @@
--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));
@@ -4451,18 +4429,12 @@
.p-2 { .p-2 {
padding: calc(var(--spacing) * 2); padding: calc(var(--spacing) * 2);
} }
.p-3 {
padding: calc(var(--spacing) * 3);
}
.p-4 { .p-4 {
padding: calc(var(--spacing) * 4); padding: calc(var(--spacing) * 4);
} }
.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);
@@ -4470,6 +4442,11 @@
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; 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 { .badge-xs {
--size: calc(var(--size-selector, 0.25rem) * 4); --size: calc(var(--size-selector, 0.25rem) * 4);
font-size: 0.625rem; font-size: 0.625rem;
@@ -4493,14 +4470,23 @@
.px-1 { .px-1 {
padding-inline: calc(var(--spacing) * 1); padding-inline: calc(var(--spacing) * 1);
} }
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 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 { .py-4 {
padding-block: calc(var(--spacing) * 4); padding-block: calc(var(--spacing) * 4);
} }
.pt-5 { .pt-1 {
padding-top: calc(var(--spacing) * 5); padding-top: calc(var(--spacing) * 1);
} }
.pt-10 { .pt-10 {
padding-top: calc(var(--spacing) * 10); padding-top: calc(var(--spacing) * 10);
@@ -4517,18 +4503,15 @@
.pb-0 { .pb-0 {
padding-bottom: calc(var(--spacing) * 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);
} }
.text-center { .text-center {
text-align: center; text-align: center;
} }
.text-left {
text-align: left;
}
.font-satoshi { .font-satoshi {
font-family: Satoshi, sans-serif; font-family: Satoshi, sans-serif;
} }
@@ -4586,6 +4569,10 @@
--tw-font-weight: var(--font-weight-light); --tw-font-weight: var(--font-weight-light);
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 { .font-semibold {
--tw-font-weight: var(--font-weight-semibold); --tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
@@ -4603,6 +4590,10 @@
.whitespace-pre-wrap { .whitespace-pre-wrap {
white-space: pre-wrap; white-space: pre-wrap;
} }
.badge-neutral {
--badge-color: var(--color-neutral);
color: var(--color-neutral-content);
}
.badge-primary { .badge-primary {
--badge-color: var(--color-primary); --badge-color: var(--color-primary);
color: var(--color-primary-content); color: var(--color-primary-content);
@@ -4625,12 +4616,21 @@
.text-base-content\/60 { .text-base-content\/60 {
color: color-mix(in oklab, var(--color-base-content) 60%, transparent); color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
} }
.text-blue-500 {
color: var(--color-blue-500);
}
.text-error { .text-error {
color: var(--color-error); color: var(--color-error);
} }
.text-gray-400 {
color: var(--color-gray-400);
}
.text-gray-500 { .text-gray-500 {
color: var(--color-gray-500); color: var(--color-gray-500);
} }
.text-gray-700 {
color: var(--color-gray-700);
}
.text-primary { .text-primary {
color: var(--color-primary); color: var(--color-primary);
} }
@@ -4672,6 +4672,9 @@
.underline { .underline {
text-decoration-line: underline; text-decoration-line: underline;
} }
.opacity-50 {
opacity: 50%;
}
.opacity-60 { .opacity-60 {
opacity: 60%; opacity: 60%;
} }
@@ -4722,6 +4725,11 @@
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration)); 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 { .ease-in {
--tw-ease: var(--ease-in); --tw-ease: var(--ease-in);
transition-timing-function: var(--ease-in); transition-timing-function: var(--ease-in);
@@ -4734,9 +4742,6 @@
--tw-ease: var(--ease-out); --tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out); transition-timing-function: var(--ease-out);
} }
.\[a-zA-Z\:\\-\] {
a-zA-Z: \-;
}
.btn-accent { .btn-accent {
--btn-color: var(--color-accent); --btn-color: var(--color-accent);
--btn-fg: var(--color-accent-content); --btn-fg: var(--color-accent-content);
@@ -4785,6 +4790,13 @@
--tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, transparent); --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\:text-gray-700 {
&:hover { &:hover {
@media (hover: 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 { .sm\:mt-4 {
@media (width >= 40rem) { @media (width >= 40rem) {
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);
@@ -4828,12 +4843,6 @@
border-bottom-left-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);
@@ -1,15 +1,24 @@
use std::{pin::Pin, time::Duration}; use std::{pin::Pin, sync::Arc, time::Duration};
use async_stream::stream;
use axum::{ use axum::{
extract::{Query, State}, extract::{Query, State},
response::{sse::Event, Sse}, response::{
sse::{Event, KeepAlive},
Sse,
},
}; };
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, TryStreamExt}; use futures::{
stream::{self, once},
Stream, StreamExt, TryStreamExt,
};
use json_stream_parser::JsonStreamParser; use json_stream_parser::JsonStreamParser;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use serde_json::from_str;
use surrealdb::{engine::any::Any, Surreal}; use surrealdb::{engine::any::Any, Surreal};
use tokio::sync::{mpsc::channel, Mutex};
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
@@ -19,7 +28,7 @@ use crate::{
create_chat_request, create_user_message, format_entities_json, LLMResponseFormat, create_chat_request, create_user_message, format_entities_json, LLMResponseFormat,
}, },
}, },
server::AppState, server::{routes::html::render_template, AppState},
storage::{ storage::{
db::{get_item, store_item, SurrealDbClient}, db::{get_item, store_item, SurrealDbClient},
types::{ types::{
@@ -29,6 +38,7 @@ use crate::{
}, },
}; };
// Error handling function
fn create_error_stream( fn create_error_stream(
message: impl Into<String>, message: impl Into<String>,
) -> Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>> { ) -> 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 // 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_clone = tx.clone();
let (tx_final, mut rx_final) = channel::<Vec<String>>(1);
// 6. Set up the collection task for DB storage // 6. Set up the collection task for DB storage
let db_client = state.surreal_db_client.clone(); let db_client = state.surreal_db_client.clone();
@@ -142,13 +153,15 @@ pub async fn get_response_stream(
} }
// Try to extract structured data // 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 let references: Vec<String> = response
.references .references
.into_iter() .into_iter()
.map(|r| r.reference) .map(|r| r.reference)
.collect(); .collect();
let _ = tx_final.send(references.clone()).await;
let ai_message = Message::new( let ai_message = Message::new(
user_message.conversation_id, user_message.conversation_id,
MessageRole::AI, MessageRole::AI,
@@ -176,7 +189,7 @@ pub async fn get_response_stream(
}); });
// Create a shared state for tracking the JSON parsing // 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 // 7. Create the response event stream
let event_stream = openai_stream let event_stream = openai_stream
@@ -185,7 +198,7 @@ pub async fn get_response_stream(
let tx_storage = tx_clone.clone(); let tx_storage = tx_clone.clone();
let json_state = json_state.clone(); let json_state = json_state.clone();
async move { stream! {
match result { match result {
Ok(response) => { Ok(response) => {
let content = response let content = response
@@ -200,29 +213,71 @@ pub async fn get_response_stream(
// Process through JSON parser // Process through JSON parser
let mut state = json_state.lock().await; let mut state = json_state.lock().await;
let display_content = state.process_chunk(&content); let display_content = state.process_chunk(&content);
drop(state); drop(state);
if !display_content.is_empty() { if !display_content.is_empty() {
return Ok(Event::default() yield Ok(Event::default()
.event("chat_message") .event("chat_message")
.data(display_content)); .data(display_content));
} }
// If display_content is empty, don't yield anything
// Empty or filtered content
Ok(Event::default().event("chat_message").data(""))
} else {
Ok(Event::default().event("chat_message").data(""))
} }
// 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) .flatten()
.chain(stream::once(async { .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() Ok(Event::default()
.event("close_stream") .event("close_stream")
.data("Stream complete")) .data("Stream complete"))
@@ -230,7 +285,7 @@ pub async fn get_response_stream(
info!("OpenAI streaming started"); info!("OpenAI streaming started");
Sse::new(event_stream.boxed()).keep_alive( Sse::new(event_stream.boxed()).keep_alive(
axum::response::sse::KeepAlive::new() KeepAlive::new()
.interval(Duration::from_secs(15)) .interval(Duration::from_secs(15))
.text("keep-alive"), .text("keep-alive"),
) )
@@ -259,7 +314,6 @@ impl StreamParserState {
} }
// Get the current state of the JSON // Get the current state of the JSON
// The get_result() method returns a &Value, not a Result
let json = self.parser.get_result(); let json = self.parser.get_result();
// Check if we're in the answer field // Check if we're in the answer field
@@ -283,46 +337,3 @@ impl StreamParserState {
String::new() 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
View 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>
+7 -5
View File
@@ -1,14 +1,16 @@
<div class="chat chat-end"> <div class="chat chat-end">
<div class="chat-header">User</div>
<div class="chat-bubble"> <div class="chat-bubble">
{{user_message.content}} {{user_message.content}}
</div> </div>
</div> </div>
<div class="chat chat-start"> <div class="chat chat-start">
<div class="chat-header">AI</div> <div hx-ext="sse" sse-connect="/chat/response-stream?message_id={{user_message.id}}" sse-close="close_stream"
<div class="chat-bubble" hx-ext="sse" sse-connect="/chat/response-stream?message_id={{user_message.id}}" hx-swap="beforeend">
sse-swap="chat_message" 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> <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>
</div> </div>
<script> <script>
+6
View 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