basic llm request stream
This commit is contained in:
1 parent
f74e4d1043
commit
a92cc76a9a
66 files changed
+1496
-158
No files matched your search
@@ -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' }),
|
||||
};
|
||||
@@ -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}`),
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user