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
@@ -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