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:
fabien committed 2026-10-05 23:46:12 +02:00
1 parent a970bfa991
commit 51bbb45b32
86 files changed
+770 -113

No files matched your search

@@ -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)}
/>
+12 -5
View File
@@ -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;
}