basic llm request stream

This commit is contained in:
fabien committed 2026-09-22 21:15:23 +02:00
1 parent f74e4d1043
commit a92cc76a9a
66 files changed
+1496 -158

No files matched your search

+62
View File
@@ -48,15 +48,77 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
return res.json();
}
async function* requestStream<T = unknown>(
path: string,
options: RequestOptions = {}
): AsyncGenerator<T, void, undefined> {
const { auth = true, headers, ...rest } = options;
const requestHeaders: HeadersInit = {
'Content-Type': 'application/json',
...headers,
};
const fetchHeaders = new Headers(requestHeaders)
if (auth) {
const token = getToken()
if (token) {
fetchHeaders.append('Authorization', `Bearer ${token}`)
}
}
const res = await fetch(`${BASE_URL}${path}`, {
...rest,
headers: fetchHeaders,
});
if (res.status === 401 ) {
logout()
goto("/login")
//throw new ApiError(401, await safeJson(res));
}
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split('\n');
buffer = lines.pop() ?? ''; // dernière ligne potentiellement incomplète
for (const line of lines) {
if (line.trim()) yield JSON.parse(line) as T;
}
}
if (buffer.trim()) yield JSON.parse(buffer) as T; // reste éventuel
} finally {
reader.releaseLock();
}
return //res.json();
}
async function safeJson(res: Response) {
try { return await res.json(); } catch { return null; }
}
export const api = {
get: <T>(path: string, opts?: RequestOptions) => request<T>(path, { ...opts, method: 'GET' }),
post: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
request<T>(path, { ...opts, method: 'POST', body: JSON.stringify(body) }),
postStream: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
requestStream<T>(path, {...opts, method: 'POST', body: JSON.stringify(body) }),
put: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
request<T>(path, { ...opts, method: 'PUT', body: JSON.stringify(body) }),
delete: <T>(path: string, opts?: RequestOptions) => request<T>(path, { ...opts, method: 'DELETE' }),
};
+11
View File
@@ -0,0 +1,11 @@
import { api } from '../client';
import type { Chat, Chunk, ChatRequest, ChatWithMessages } from "$lib/types/api"
export const chatApi = {
createChat: (space: string, data: Partial<ChatRequest>) => api.post<Chat>(`/chat/${space}`, data),
createChatMessage: (space: string, id: string, data: Partial<ChatRequest>) => api.postStream<Chunk>(`/chat/${space}/${id}`, data),
allChats: (space: string, pageNum: number) => api.get<Chat[]>(`/chat/${space}/all/${pageNum}`),
getChat: (space: string, id: string) => api.get<ChatWithMessages>(`/chat/${space}/${id}`),
subscribe: (space: string, id: string) => api.get<ChatWithMessages>(`/chat/${space}/${id}/subscribe`),
deleteChat: (space: string, id: string) => api.delete(`/chat/${space}/${id}`),
};
+4 -2
View File
@@ -5,8 +5,10 @@
const closeDrawer = (e: Event) => {
e.preventDefault()
uiState.sidebarMobileOpen=false
uiState.menuContentVisible=false
if (uiState.sidebarMobileOpen) {
uiState.sidebarMobileOpen=false
uiState.menuContentVisible=false
}
goto(link)
}
</script>
+50
View File
@@ -78,6 +78,49 @@ export interface Agent {
_version: string
}
export type Chat = {
id: string
teamId: string
startDatetime: string
endDatetime: string
agentId: string
userId: string
taskId: string
title: string
_date_created: string
_date_updated: string
_version: string
}
export type Message = {
id: string
role: string
content: string
toolCallId: string
toolCalls: Array<string>
_date_created: string
_date_updated: string
_version: string
}
export type ChatWithMessages = {
chat: Chat
messages: Message[]
}
export type Chunk = {
done: boolean
role: string
content: string
}
export type ChatRequest = {
agentId: string
providerId: string
modelId: string
prompt: string
}
export enum SaveState {
None,
@@ -86,3 +129,10 @@ export enum SaveState {
Saved,
Error,
}
export type PageRequest = {
pageNum: number
pageSize: number
filters: Map<string, string>
}
+3
View File
@@ -0,0 +1,3 @@
export const autofocus = (node: HTMLElement) => {
node.focus();
}
@@ -1,8 +1,35 @@
<div class="container">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
<script lang="ts">
import {autofocus} from "$lib/ui/autofocus"
import {chatApi} from "$lib/api/endpoints/chat"
import type {Chat} from "$lib/types/api"
import type { PageProps } from './$types';
import { goto } from "$app/navigation";
let { data }: PageProps = $props();
let chat = $state({} as Chat)
let prompt = $state("")
const handleOnSubmit = async () => {
chatApi.createChat(data.spaceId, chat)
.then((result)=>{
goto(`/${data.spaceId}/${result.id}`, {state: {prompt: prompt }})
}).catch((err)=>console.log(err))
}
</script>
<div class="h-full w-full flex items-center public">
<div class="container">
<form class="request flex w-full" onsubmit={handleOnSubmit}>
<input
class="chatinput w-full flex-1 focus:outline-hidden focus:shadow-none focus:ring-0"
type="text"
placeholder="Demande ce que tu veux"
bind:value={prompt}
use:autofocus
/>
<button class="chatbutton">Go</button>
</form>
</div>
</div>
@@ -1,3 +1,8 @@
export function load() {
return { title: 'Conversation' };
import type { PageLoad } from "./$types";
export const load: PageLoad = ({params}) => {
return {
title: 'Nouvelle conversation',
spaceId : params.space,
};
}
@@ -0,0 +1,75 @@
<script lang="ts">
import {autofocus} from "$lib/ui/autofocus"
import { onMount } from "svelte"
import {page} from "$app/state"
import {chatApi} from "$lib/api/endpoints/chat"
import type { PageProps } from './$types';
import type { Message, Chunk } from "$lib/types/api"
import SvelteMarkdown from '@humanspeak/svelte-markdown'
let prompt = $state("")
let { data }: PageProps = $props();
let messages = $state([] as Message[])
onMount(async ()=>{
if (page.state.prompt) {
console.log(page.state.prompt)
await sendRequest(page.state.prompt)
}
})
let historyDiv: HTMLDivElement;
$effect(() => {
if (historyDiv) {
console.log("effect")
historyDiv.scrollTop = historyDiv.scrollHeight;
}
});
const sendRequest = async(theprompt) => {
prompt = ""
let lastrole=""
for await (const chunk of chatApi.createChatMessage(data.spaceId, data.chatId, { prompt: theprompt })) {
if (chunk.role!=lastrole) {
const currentDate = new Date()
messages.push({
id: currentDate.getTime(),
role: chunk.role,
content: chunk.content,
})
lastrole=chunk.role
} else {
messages[messages.length-1].content += chunk.content
}
historyDiv.scrollTop = historyDiv.scrollHeight;
}
}
const handleOnSubmit = async ()=>{
await sendRequest(prompt)
}
</script>
<div class="h-full w-full flex flex-col">
<div id="history" class="flex-1 container overflow-auto" bind:this={historyDiv}>
{#each messages as message (message.id)}
<div class={["message", "rounded", message.role]}>
<div class="message-content">
<SvelteMarkdown source={message.content} />
</div>
</div>
{/each}
</div>
<div id="prompt" class="container">
<form class="request flex w-full" onsubmit={handleOnSubmit}>
<input
class="chatinput w-full flex-1 focus:outline-hidden focus:shadow-none focus:ring-0"
type="text"
placeholder="..."
bind:value={prompt}
use:autofocus
/>
<button class="chatbutton">Go</button>
</form>
</div>
</div>
@@ -0,0 +1,9 @@
import type { PageLoad } from "./$types";
export const load: PageLoad = ({params}) => {
return {
title: 'Nouvelle conversation',
spaceId: params.space,
chatId: params.conversation,
};
}
@@ -100,7 +100,6 @@
let updSpace = space
updSpace.defaultAgentId = agentId
teamApi.updateSpace(updSpace.id, updSpace).then((newdata)=>{
console.log(newdata)
spaceState.spaces.map((s)=>s.id===spaceState.current?newdata:s)
space=newdata
}).catch((err)=>{
@@ -3,6 +3,7 @@
import { auth, setToken } from '$lib/state/auth.svelte';
import { goto } from '$app/navigation';
import { authApi } from "$lib/api/endpoints/auth"
import { validateEmail } from "$lib/utilities/validators"
type Credentials = {
email: string
@@ -17,7 +18,7 @@
})
const formSubmitEmail = () => {
credentials.validemail = true
credentials.validemail = validateEmail(credentials.email)
}
const formSubmitPass = (e: Event) => {
@@ -43,16 +44,16 @@
<form onsubmit={formSubmitEmail}>
<div class="flex">
<label for="email" class="block mb-2 flex-1">Email</label>
<a href="/recover-password" class="text-xs">Mot de passe oublié ?</a>
<a href="/recover-password" class="text-xs ">Mot de passe oublié ?</a>
</div>
<div class="flex flex-row">
<input
type="text"
bind:value={credentials.email}
class="textinput mr-1"
class="textinput mr-1 flex-1"
autofocus
/>
<button class="bt" title="page suivante">
<button class="bt small" title="page suivante">
<i class="icon-right-big"></i>
</button>
</div>
@@ -72,10 +73,10 @@
<input
type="password"
bind:value={credentials.password}
class="textinput mr-1"
class="textinput mr-1 flex-1"
autofocus
/>
<button class="bt">
<button class="bt small">
Entrer
</button>
</div>
+46 -8
View File
@@ -11,7 +11,7 @@ body {
@apply h-full bg-stone-100 text-stone-950 dark:bg-stone-950 dark:text-stone-100;
}
.formelement > label {
label {
@apply text-xs text-stone-600 uppercase dark:text-stone-400;
}
hr {
@@ -30,9 +30,9 @@ h2 {
}
.bt {
appearance: button;
@apply min-w-40 cursor-pointer rounded bg-stone-300 px-4 py-1 transition-all duration-300 hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700;
@apply min-w-40 cursor-pointer rounded bg-stone-300 px-4 py-1 transition-all duration-300 hover:bg-stone-400 dark:bg-stone-800 dark:hover:bg-stone-700;
&.small {
@apply min-w-0 px-2 py-1;
@apply min-w-2 px-2 py-1;
}
&.changed,
&.saving {
@@ -62,10 +62,12 @@ h2 {
.public h1 {
@apply mb-4 text-3xl font-bold;
}
.profile a,
.public a {
.profile a {
@apply bg-sky-300 dark:bg-sky-700;
}
.public a {
@apply text-sky-500 dark:text-sky-500;
}
#outer {
@apply flex h-full flex-row;
@@ -113,10 +115,18 @@ h2 {
}
}
.chatinput {
@apply w-full rounded-l-xl border-stone-300 bg-stone-50 p-2 text-stone-900 dark:border-stone-700 dark:bg-stone-900 dark:text-stone-100;
}
.chatbutton {
@apply min-w-12 cursor-pointer rounded-r-xl bg-stone-300 px-2 py-1 transition-all duration-300 hover:bg-stone-400 dark:bg-stone-700 dark:hover:bg-stone-600;
}
/* form */
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 text-sm dark:border-stone-700 dark:bg-stone-900;
@apply rounded border border-stone-300 bg-stone-200 p-2 text-sm dark:border-stone-700 dark:bg-stone-900;
}
.formelement {
@@ -124,11 +134,11 @@ h2 {
}
.textinput {
@apply h-8 w-full rounded border-stone-700 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
@apply h-8 w-full rounded border-stone-300 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:border-stone-700 dark:bg-stone-950 dark:text-stone-100;
}
.areainput {
@apply w-full rounded border-stone-700 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
@apply w-full rounded border-stone-300 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
/* svelte select */
@@ -261,3 +271,31 @@ h2 {
.list-item {
@apply list-none;
}
/* chat */
.message {
@apply m-2 rounded p-2;
&.user {
@apply bg-sky-500 dark:text-white;
}
&.assistant {
&.reasoning {
@apply text-white/50;
}
}
}
.message-content {
@apply text-sm;
}
.message-content p {
@apply my-2;
}
.message-content pre {
@apply my-2 whitespace-pre dark:bg-stone-700;
}