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

@@ -3,6 +3,7 @@ package turso
import (
"database/sql"
"log"
"strings"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
@@ -29,15 +30,21 @@ func NewTursoChatRepository(db *sql.DB) *TursoChatRepository {
}
}
func (r *TursoChatRepository) ListChats(userID string, teamID string, page int) ([]*domain.Chat, error) {
func (r *TursoChatRepository) ListChats(userID string, teamID string, page int, query string) ([]*domain.Chat, error) {
where := "team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)"
args := []any{teamID, userID}
if query != "" {
// % et _ sont des jokers LIKE : on les neutralise
escaped := strings.ReplaceAll(query, `\`, `\\`)
escaped = strings.ReplaceAll(escaped, `%`, `\%`)
escaped = strings.ReplaceAll(escaped, `_`, `\_`)
where += " and title like $3 escape '\\'"
args = append(args, "%"+escaped+"%")
}
return r.chatTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
orm.WithWhere(where, args...),
orm.WithOrder("_date_created desc"),
orm.WithPagination(page, 50),
orm.WithPagination(page, 20),
)
}
@@ -68,6 +75,16 @@ func (r *TursoChatRepository) UpdateChat(userID string, chat *domain.Chat) (*dom
}
func (r *TursoChatRepository) DeleteChat(userID string, teamID string, id string) error {
// les messages n'ont pas de clé étrangère cascade : suppression explicite
err := r.messageTable.DeleteWhere(
"chat_id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
if err != nil {
return err
}
return r.chatTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
@@ -0,0 +1,100 @@
package turso
import (
"database/sql"
"testing"
_ "github.com/mattn/go-sqlite3"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
func TestListChatsFilter(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := `
create table chats (
id text not null primary key, team_id text not null, start_datetime numeric,
end_datetime numeric, agent_id text, user_id text, task_id text, title text,
fresh_title numeric, _date_created numeric not null, _date_updated numeric,
_version text not null);
create table user_teams (user_id text, team_id text);
insert into chats (id, team_id, user_id, agent_id, task_id, title, fresh_title, _date_created, _version)
values ('c1','t1','u1','','','Problème de timeout',0,'2026-01-01 10:00:00','v'),
('c2','t1','u1','','','Discussion navigateur',0,'2026-01-02 10:00:00','v'),
('c3','t1','u1','','','Recette de cuisine 100%',0,'2026-01-03 10:00:00','v');
insert into user_teams (user_id, team_id) values ('u1','t1');
insert into chats (id, team_id, user_id, agent_id, task_id, title, fresh_title, _date_created, _version)
select 'bulk-'||(n+1), 't1','u1','','','Conversation '||(n+1), 0,
datetime('2026-01-01','+'||(n+10)||' minutes'), 'v'
from (with recursive cnt(n) as (select 0 union all select n+1 from cnt where n < 24) select n from cnt);
`
if _, err := db.Exec(schema); err != nil {
t.Fatal(err)
}
repo := NewTursoChatRepository(db)
// sans filtre, page 0 : 20 conversations (28 en base)
p0, err := repo.ListChats("u1", "t1", 0, "")
if err != nil {
t.Fatal(err)
}
if len(p0) != 20 {
t.Fatalf("page 0: expected 20, got %d", len(p0))
}
p1, err := repo.ListChats("u1", "t1", 1, "")
if err != nil {
t.Fatal(err)
}
if len(p1) != 8 {
t.Fatalf("page 1: expected 8, got %d", len(p1))
}
seen := map[string]bool{}
for _, c := range append(append([]*domain.Chat{}, p0...), p1...) {
if seen[c.ID] {
t.Fatalf("chat %s appears twice across pages", c.ID)
}
seen[c.ID] = true
}
p2, err := repo.ListChats("u1", "t1", 2, "")
if err != nil {
t.Fatal(err)
}
if len(p2) != 0 {
t.Fatalf("page 2: expected 0, got %d", len(p2))
}
// filtre simple
res, err := repo.ListChats("u1", "t1", 0, "timeout")
if err != nil {
t.Fatal(err)
}
if len(res) != 1 || res[0].ID != "c1" {
t.Fatalf("filter 'timeout': %+v", res)
}
// jokers LIKE neutralisés : '100%' ne doit rien matcher de spécial
res, err = repo.ListChats("u1", "t1", 0, "100%")
if err != nil {
t.Fatal(err)
}
if len(res) != 1 || res[0].ID != "c3" {
t.Fatalf("filter '100%%': %+v", res)
}
// pas de résultat
res, err = repo.ListChats("u1", "t1", 0, "inexistant")
if err != nil {
t.Fatal(err)
}
if len(res) != 0 {
t.Fatalf("expected 0, got %d", len(res))
}
_ = domain.Chat{}
}