compaction de contexte, filtre/pagination/suppression des conversations
- compaction automatique du contexte LLM quand la taille atteint le seuil (contexte max - taille de réponse max), par résumé de l'historique ancien - filtre textuel (?q=) et pagination (20/page) de l'historique des conversations - correctif ORM : offset de pagination en base 0 - suppression d'une conversation : messages associés supprimés, session en cours annulée, popup de confirmation (ConfirmDialog) - titres de conversation nettoyés du markdown - styles des listes markdown, variante danger du bouton - go.mod/go.sum : dépendances go-rod et stealth manquantes du commit précédent
This commit is contained in:
1 parent
a970bfa991
commit
51bbb45b32
86 files changed
+770
-113
No files matched your search
@@ -4,7 +4,8 @@ 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}`),
|
||||
allChats: (space: string, pageNum: number, query?: string) =>
|
||||
api.get<Chat[]>(`/chat/${space}/all/${pageNum}${query ? `?q=${encodeURIComponent(query)}` : ''}`),
|
||||
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}`),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { fade, scale } from "svelte/transition";
|
||||
|
||||
let {
|
||||
open = false,
|
||||
title = "",
|
||||
message = "",
|
||||
confirmLabel = "Confirmer",
|
||||
cancelLabel = "Annuler",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
open?: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
onConfirm?: () => void;
|
||||
onCancel?: () => void;
|
||||
} = $props();
|
||||
|
||||
const close = () => onCancel?.();
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (open && e.key === "Escape") close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if open}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-0 cursor-default bg-black/50"
|
||||
aria-label="Fermer"
|
||||
onclick={close}
|
||||
transition:fade={{ duration: 150 }}
|
||||
></button>
|
||||
<div
|
||||
class="relative w-full max-w-sm rounded-lg bg-stone-50 p-4 shadow-xl dark:bg-stone-900"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
transition:scale={{ duration: 150, start: 0.95 }}
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 dark:bg-red-950 dark:text-red-400">
|
||||
<i class="icon-trash-empty"></i>
|
||||
</div>
|
||||
<h2 id="confirm-dialog-title" class="text-base font-bold">{title}</h2>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-stone-600 dark:text-stone-400">{message}</p>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" class="bt small" onclick={close}>{cancelLabel}</button>
|
||||
<button type="button" class="bt small danger" onclick={onConfirm}>{confirmLabel}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,15 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from "$app/navigation";
|
||||
import { chatApi } from "$lib/api/endpoints/chat";
|
||||
import { putMessage } from "$lib/state/toaster.svelte";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
import type { PageProps } from './$types';
|
||||
import type { Chat } from "$lib/types/api";
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
let search = $state("");
|
||||
// svelte-ignore state_referenced_locally -- la liste est gérée localement
|
||||
// (filtre, pagination, suppression) et ne doit pas suivre les rechargements de data
|
||||
let conversations = $state<Chat[]>(data.conversations);
|
||||
let page = $state(0);
|
||||
let loading = $state(false);
|
||||
let conversationToDelete = $state<Chat | null>(null);
|
||||
|
||||
const hasMore = $derived(conversations.length === (page + 1) * PAGE_SIZE);
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchTimer !== undefined) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(async () => {
|
||||
page = 0;
|
||||
const result = await chatApi.allChats(data.space, 0, search);
|
||||
conversations = result ?? [];
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
const result = await chatApi.allChats(data.space, page + 1, search);
|
||||
const next = result ?? [];
|
||||
conversations = [...conversations, ...next];
|
||||
page += 1;
|
||||
} catch {
|
||||
putMessage("failure", "Erreur lors du chargement des conversations");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!conversationToDelete) return;
|
||||
try {
|
||||
await chatApi.deleteChat(data.space, conversationToDelete.id);
|
||||
putMessage("success", "Conversation supprimée");
|
||||
await invalidateAll();
|
||||
const result = await chatApi.allChats(data.space, 0, search);
|
||||
conversations = result ?? [];
|
||||
page = 0;
|
||||
} catch {
|
||||
putMessage("failure", "Erreur lors de la suppression de la conversation");
|
||||
} finally {
|
||||
conversationToDelete = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each data.conversations as conversation (conversation.id)}
|
||||
<div class="formelement my-2">
|
||||
<input
|
||||
class="textinput"
|
||||
type="search"
|
||||
placeholder="Rechercher une conversation..."
|
||||
bind:value={search}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#each conversations as conversation (conversation.id)}
|
||||
<div class="dark:bg-stone-800 rounded p-2 my-2">
|
||||
<div class="font-bold text-base">
|
||||
<a href={`/${data.space}/${conversation.id}`}>{conversation.title}</a>
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex-1 font-bold text-base">
|
||||
<a href={`/${data.space}/${conversation.id}`}>{conversation.title}</a>
|
||||
</div>
|
||||
<button
|
||||
class="bt small"
|
||||
title="Supprimer la conversation"
|
||||
onclick={() => (conversationToDelete = conversation)}
|
||||
>
|
||||
<i class="icon-trash-empty"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-xs">{conversation._date_created}</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if conversations.length === 0}
|
||||
<p class="my-4 text-center text-sm text-stone-500">Aucune conversation trouvée.</p>
|
||||
{/if}
|
||||
|
||||
{#if hasMore}
|
||||
<div class="my-4 text-center">
|
||||
<button class="bt small" disabled={loading} onclick={loadMore}>
|
||||
{loading ? "Chargement..." : "Charger plus"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={conversationToDelete !== null}
|
||||
title="Supprimer la conversation"
|
||||
message={`Voulez-vous vraiment supprimer « ${conversationToDelete?.title} » ? Cette action est irréversible.`}
|
||||
confirmLabel="Supprimer"
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => (conversationToDelete = null)}
|
||||
/>
|
||||
@@ -44,6 +44,9 @@ h2 {
|
||||
&.error {
|
||||
@apply bg-red-300 dark:bg-red-700;
|
||||
}
|
||||
&.danger {
|
||||
@apply bg-red-500 text-white hover:bg-red-600 dark:bg-red-700 dark:hover:bg-red-600;
|
||||
}
|
||||
&.disabled {
|
||||
@apply text-stone-400 hover:bg-stone-200 dark:hover:bg-stone-800;
|
||||
}
|
||||
@@ -292,7 +295,15 @@ h2 {
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@apply my-2;
|
||||
@apply my-2 pl-5;
|
||||
}
|
||||
|
||||
ul li {
|
||||
@apply list-disc pl-2;
|
||||
}
|
||||
|
||||
ol li {
|
||||
@apply list-decimal pl-2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -315,10 +326,6 @@ h2 {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply ml-5 list-disc pl-4 marker:text-xl;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply my-2 rounded px-2 py-1 text-xs dark:bg-stone-700;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user