Compare commits

...
19 Commits
Author SHA1 Message Date
fabien 5863858b09 fix panic : appels OnChunk sans garde de nil dans AnyLLMEngine.Stream (crash du serveur lors d'un appel d'outil hors streaming, ex. tâche planifiée) 2026-10-06 23:46:17 +02:00
fabien 535603f6d3 README : installation, configuration, authentification OpenID Connect 2026-10-06 22:15:59 +02:00
fabien 058370830f tâches planifiées : prompts cron persistés, outil scheduler, conversations liées aux tâches, rattrapage des exécutions manquées au redémarrage (go-cron MissedRunOnce) 2026-10-06 22:15:59 +02:00
fabien bedb6c98e7 authentification OpenID Connect (Keycloak) : flux authorization code, state signé, validation du token ID via JWKS, provisioning automatique des utilisateurs 2026-10-06 22:03:20 +02:00
fabien 51bbb45b32 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
2026-10-05 23:46:12 +02:00
fabien a970bfa991 correction bug nouvelle conversation 2026-10-05 22:21:57 +02:00
fabien 509a3b14c8 Mise à jour du titre (imparfait) 2026-10-05 22:05:07 +02:00
fabien 345129c7bf Ajout de SEARXNG pour les recherches web 2026-10-05 19:05:24 +02:00
fabien 87eeb41364 paufinement de l'UI du chat 2026-10-04 21:55:31 +02:00
fabien 0a6ffab050 prise en charge des outils + weather 2026-10-04 21:03:51 +02:00
fabien a36f1231cd basic system promp 2026-09-23 00:09:26 +02:00
fabien b3be1a81e6 basic system prompt 2026-09-22 23:34:28 +02:00
fabien 1b5abb566e accès à un chat ouvert 2026-09-22 23:01:20 +02:00
fabien a92cc76a9a basic llm request stream 2026-09-22 21:15:23 +02:00
fabien f74e4d1043 Agent par défaut 2026-09-07 21:04:48 +02:00
fabien cf10b9073b divers fixes : bouton sauvegarde 2026-09-05 15:18:10 +02:00
fabien f1b176ae4a fixes 2026-09-05 11:15:36 +02:00
fabien 0bb8d1cb46 correction des pb de space : le spaceId est maintenant dans l'url uniquement 2026-09-02 21:50:26 +02:00
fabien c52460bac0 gestion des espaces dans l'url 2026-09-01 19:45:29 +02:00
239 changed files with 9048 additions and 544 deletions

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
# Lassistanoque
Assistant IA auto-hébergé : backend Go (API + SPA embarquée), base SQLite, connecteurs LLM multiples (via [any-llm](https://github.com/mozilla-ai/any-llm-go)), authentification par mot de passe ou OpenID Connect.
## Prérequis
- Go >= 1.26
- Node.js + npm (build du frontend)
- (optionnel) un serveur OpenID Connect, ex. [Keycloak](https://www.keycloak.org/)
## Installation
```bash
# 1. frontend : build la SPA et l'écrit dans backend/internal/http/web
cd backend/web
npm install
npm run build
# 2. backend
cd ..
go build -o lassistanoque .
```
Le `Makefile` du backend enchaîne les deux et produit les binaires multi-plateformes dans `build/` :
```bash
make build # linux/amd64, linux/arm64, windows, darwin...
```
## Configuration
La configuration se fait par variables d'environnement, chargeables depuis un fichier `.env` placé dans le répertoire de lancement (backend) :
| Variable | Défaut | Description |
|----------------------------|--------------------|----------------------------------------------------|
| `LASSISTANOQUE_DB_TYPE` | `turso` | Type de base (seul `turso`/SQLite est implémenté) |
| `LASSISTANOQUE_DB_URL` | `lassistanoque.db` | Chemin du fichier SQLite |
| `LASSISTANOQUE_HTTP_PORT` | `3000` | Port du serveur HTTP |
| `LASSISTANOQUE_JWT_SECRET` | *(obligatoire)* | Secret de signature des tokens, ex. `openssl rand -base64 32` |
| `LASSISTANOQUE_STORAGE_TYPE`| `database` | Stockage des fichiers (seul `database` est implémenté) |
| `LASSISTANOQUE_SERVER_ID` | `default` | Identifiant du serveur |
Exemple `backend/.env` :
```env
LASSISTANOQUE_JWT_SECRET=<secret généré>
LASSISTANOQUE_HTTP_PORT=3000
```
## Lancement
```bash
./lassistanoque serve
```
- Les migrations de la base sont appliquées automatiquement au démarrage (ou manuellement avec `./lassistanoque migrate`).
- L'application est servie sur `http://localhost:3000` (SPA + API `/api`).
- Créez le premier compte depuis la page d'inscription (`/register`), puis configurez un fournisseur LLM et sa clé d'API dans les réglages de l'espace (`settings/models`).
## Authentification OpenID Connect
Les fournisseurs OIDC sont enregistrés dans la table `oidc` :
```sql
insert into oidc (id, label, domain, client_id, client_secret, wellknown_url, _date_created, _version)
values (
'keycloak', -- identifiant du fournisseur
'Keycloak', -- libellé affiché sur la page de connexion
'sso.example.com', -- domaine du fournisseur
'lassistanoque', -- client_id
'<client_secret>',
'https://<keycloak>/realms/<realm>/.well-known/openid-configuration',
datetime('now'),
'v1'
);
```
Côté fournisseur (Keycloak) :
- client de type *confidential*, *Standard Flow* (authorization code) activé ;
- *Valid Redirect URIs* : `http://localhost:3000/api/auth/oidc/callback` (l'URL de callback est reconstruite depuis le `Host` de la requête ; derrière un reverse proxy, transmettre `X-Forwarded-Proto` et `X-Forwarded-Host`).
Flux : les fournisseurs configurés apparaissent comme boutons sur la page de connexion → redirection vers le fournisseur → au retour, le backend valide le `state` signé, échange le code, vérifie le token ID (signature JWKS, émetteur, audience) puis délivre les tokens applicatifs. À la première connexion, l'utilisateur est créé automatiquement avec son espace personnel.
## Développement
```bash
# backend (API sur :3000)
cd backend
go run . serve
# frontend en mode dev (Vite sur :5173, API proxyfiée vers :3000)
cd web
npm run dev
```
Structure :
```
backend/
├── cmd/ # CLI (serve, migrate, version) et assemblage IoC
├── internal/
│ ├── adapter/ # implémentations : db (turso/SQLite), auth (password, oidc), llm, tools, security (JWT)
│ ├── domain/ # entités et interfaces des repositories
│ ├── http/ # serveur HTTP, routes, SPA embarquée
│ └── service/ # logique métier (auth, chat, agents, tasks...)
└── web/ # frontend SvelteKit
```
Tests : `go test ./backend/...`
+102
View File
@@ -0,0 +1,102 @@
<mxfile host="Electron">
<diagram name="Page-1" id="6ee6LexvoJe0i-CFFPA3">
<mxGraphModel dx="1067" dy="848" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-29" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=light-dark(#FFFFFF,transparent);strokeColor=#7D7D7D;" value="" vertex="1">
<mxGeometry height="270" width="220" x="540" y="140" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-15" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-1" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatService" vertex="1">
<mxGeometry height="120" width="140" x="290" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-2" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat -&amp;gt; Chat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-3" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="AddMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-22" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="getChat" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-16" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-5" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="BrainHandler" vertex="1">
<mxGeometry height="150" width="140" x="590" y="450" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-6" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="ProcessResponse" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-24" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="subscribe" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-7" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Start" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-8" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Stop" vertex="1">
<mxGeometry height="30" width="140" y="120" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-10" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-11" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatRepository" vertex="1">
<mxGeometry height="90" width="140" x="400" y="710" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-12" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-13" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-21" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-23" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-17" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="WebHandler" vertex="1">
<mxGeometry height="120" width="140" x="30" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-18" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-25" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-19" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="GetChatWithMessages" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-26" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="240" y="400" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-27" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-24">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-28" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-38" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="RunningSession" vertex="1">
<mxGeometry height="120" width="140" x="570" y="230" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-39" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 1" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-40" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 2" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-41" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 3" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-42" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Goroutine" vertex="1">
<mxGeometry height="30" width="60" x="560" y="150" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+108
View File
@@ -0,0 +1,108 @@
<mxfile host="Electron">
<diagram name="Page-1" id="6ee6LexvoJe0i-CFFPA3">
<mxGraphModel dx="1067" dy="848" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-43" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="250" width="510" x="260" y="380" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-29" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=light-dark(#FFFFFF,transparent);strokeColor=#7D7D7D;" value="" vertex="1">
<mxGeometry height="270" width="220" x="520" y="50" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-15" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-1" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatService" vertex="1">
<mxGeometry height="120" width="140" x="290" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-2" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat -&amp;gt; Chat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-3" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="AddMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-22" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="getChat" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-16" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-5" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="BrainHandler" vertex="1">
<mxGeometry height="150" width="140" x="590" y="450" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-6" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="ProcessResponse" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-24" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="subscribe" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-7" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Start" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-8" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Stop" vertex="1">
<mxGeometry height="30" width="140" y="120" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-10" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-11" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatRepository" vertex="1">
<mxGeometry height="90" width="140" x="400" y="710" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-12" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-13" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-21" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-23" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-17" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="WebHandler" vertex="1">
<mxGeometry height="120" width="140" x="30" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-18" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-25" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-19" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="GetChatWithMessages" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-26" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="240" y="400" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-27" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-24">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-28" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-38" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="RunningSession" vertex="1">
<mxGeometry height="120" width="140" x="550" y="140" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-39" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 1" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-40" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 2" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-41" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 3" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-42" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Goroutine" vertex="1">
<mxGeometry height="30" width="60" x="540" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-44" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Fusionner" vertex="1">
<mxGeometry height="30" width="60" x="480" y="380" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+163
View File
@@ -0,0 +1,163 @@
package cmd
import (
"time"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/oidc"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/browser"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/scheduler"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/searxng"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/agent"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/chat"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/task"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
var db database.Database
// adapters
var storeAdapter storage.StorageProvider
var llmengine domain.LLMEngine
// services
var authService *auth.Service
var userService *user.Service
var storageService *storage.Service
var providerService *provider.Service
var agentService *agent.Service
var toolService *tool.Service
var chatService *chat.Service
var taskService *task.Service
var tokenManager auth.TokenManager
func init() {
var err error
db, err = database.GetDatabase()
if err != nil {
panic(err)
}
err = db.Migrate()
if err != nil {
panic(err)
}
}
func Close() {
db.Close()
}
// Adapters
func getStoreAdapter() storage.StorageProvider {
if storeAdapter == nil {
var err error
storeAdapter, err = file.GetStorageProvider(db.FileRepository())
if err != nil {
panic(err)
}
}
return storeAdapter
}
func getAuthenticators() map[string]auth.Authenticator {
authenticators := make(map[string]auth.Authenticator)
pwdAuth := password.NewPasswordAuthenticator(GetTokenManager(), db.UserRepository())
authenticators["password"] = pwdAuth
oidcAuth := oidc.NewOIDCAuthenticator(GetTokenManager(), db.OIDCProviderRepository(), db.UserRepository())
authenticators["oidc"] = oidcAuth
return authenticators
}
func GetTokenManager() auth.TokenManager {
if tokenManager == nil {
tokenManager = security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
}
return tokenManager
}
func GetLLMAdapter() domain.LLMEngine {
if llmengine == nil {
llmengine = llm.NewAnyLLMEngine()
}
return llmengine
}
// Services
func GetStorageService() *storage.Service {
if storageService == nil {
storageService = storage.NewService(getStoreAdapter())
}
return storageService
}
func GetAuthService() *auth.Service {
if authService == nil {
authService = auth.NewService(db.SettingsRepository(), db.UserRepository(), db.OIDCProviderRepository(), GetTokenManager(), getAuthenticators())
}
return authService
}
func GetUserService() *user.Service {
if userService == nil {
userService = user.NewService(db.UserRepository())
}
return userService
}
func GetProviderService() *provider.Service {
if providerService == nil {
providerService = provider.NewService(db.ProviderRepository(), db.UserRepository(), GetLLMAdapter())
}
return providerService
}
func GetAgentService() *agent.Service {
if agentService == nil {
agentService = agent.NewService(db.AgentRepository(), db.UserRepository())
}
return agentService
}
func GetChatService() *chat.Service {
if chatService == nil {
chatService = chat.NewService(
db.UserRepository(),
db.AgentRepository(),
db.ProviderRepository(),
db.ChatRepository(),
GetLLMAdapter(),
GetToolService(),
)
// l'exécution des tâches planifiées passe par le service de chat
GetTaskService().SetChatService(chatService)
}
return chatService
}
func GetTaskService() *task.Service {
if taskService == nil {
taskService = task.NewService(db.TaskRepository())
}
return taskService
}
func GetToolService() *tool.Service {
if toolService == nil {
toolService = tool.NewService(db.ToolRepository(), db.UserRepository())
toolService.RegisterToolImpl(&weather.WeatherTool{})
toolService.RegisterToolImpl(&searxng.SearxngTool{})
toolService.RegisterToolImpl(&browser.BrowserTool{})
toolService.RegisterToolImpl(scheduler.New(GetTaskService()))
}
return toolService
}
+58 -50
View File
@@ -1,20 +1,18 @@
package cmd
import (
"context"
"errors"
"log/slog"
nethttp "net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/gateway"
"trankilou.fr/lassistanoque/backend/internal/http"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func init() {
@@ -37,51 +35,61 @@ func newServeCmd() *cobra.Command {
}
func runServe() {
// database
db, err := database.GetDatabase()
if err != nil {
panic(err)
}
defer db.Close()
err = db.Migrate()
if err != nil {
panic(err)
}
// storage
store, err := file.GetStorageProvider(db.FileRepository())
if err != nil {
panic(err)
}
// Adapters
tokenManager := security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
pwdAuth := password.NewPasswordAuthenticator(tokenManager, db.UserRepository())
llmEngine := llm.NewAnyLLMEngine()
// services
authService := auth.NewService(
db.SettingsRepository(),
db.UserRepository(),
map[string]auth.Authenticator{
"password": pwdAuth,
},
)
userService := user.NewService(db.UserRepository())
storageService := storage.NewService(store)
providerService := provider.NewService(db.ProviderRepository(), db.UserRepository(), llmEngine)
defer Close()
// http server
router := http.NewRouter(http.Dependencies{
StorageService: storageService,
AuthService: authService,
UserService: userService,
TokenManager: tokenManager,
ProviderService: providerService,
httpRouter := http.NewRouter(http.Dependencies{
StorageService: GetStorageService(),
AuthService: GetAuthService(),
UserService: GetUserService(),
ProviderService: GetProviderService(),
AgentService: GetAgentService(),
ChatService: GetChatService(),
ToolService: GetToolService(),
TaskService: GetTaskService(),
TokenManager: GetTokenManager(),
})
if err := router.Start(); err != nil {
slog.Error("failed to start server", "error", err)
gatewayRouter := gateway.NewRouter(gateway.Dependencies{})
// task scheduler : replanifie les tâches persistées
GetTaskService().Start()
defer GetTaskService().Stop()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// start Brain router
go func() {
if err := gatewayRouter.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) {
slog.Error("failed to start llm server", "error", err)
}
}()
// start HTTP router
go func() {
if err := httpRouter.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) {
slog.Error("failed to start http server", "error", err)
}
}()
<-ctx.Done()
// stop HTTP service
ctx, cancel1 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel1()
if err := httpRouter.Stop(ctx); err != nil {
slog.Error("failed to stop http server", "error", err)
}
slog.Info("HTTP server stopped gracefully")
// stop LLM service
ctx, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel2()
if err := gatewayRouter.Stop(ctx); err != nil {
slog.Error("failed to stop llm server", "error", err)
}
slog.Info("LLM server stopped gracefully")
}
@@ -0,0 +1,88 @@
package oidc
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
// providerMetadata est le document de découverte OpenID
// (.well-known/openid-configuration).
type providerMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksURI string `json:"jwks_uri"`
}
type metadataCacheEntry struct {
metadata *providerMetadata
expiresAt time.Time
}
// discovery maintient un cache des documents de découverte par fournisseur.
type discovery struct {
httpClient *http.Client
mu sync.Mutex
entries map[string]*metadataCacheEntry
}
func newDiscovery() *discovery {
return &discovery{
httpClient: &http.Client{Timeout: 10 * time.Second},
entries: make(map[string]*metadataCacheEntry),
}
}
// Metadata retourne les endpoints du fournisseur, avec mise en cache.
func (d *discovery) Metadata(provider *domain.OIDCProvider) (*providerMetadata, error) {
d.mu.Lock()
entry, ok := d.entries[provider.ID]
if ok && time.Now().Before(entry.expiresAt) {
d.mu.Unlock()
return entry.metadata, nil
}
d.mu.Unlock()
metadata, err := d.fetch(provider.WellknownURL)
if err != nil {
return nil, fmt.Errorf("error fetching provider discovery document: %s", err)
}
if metadata.Issuer == "" || metadata.AuthorizationEndpoint == "" || metadata.TokenEndpoint == "" || metadata.JwksURI == "" {
return nil, fmt.Errorf("incomplete discovery document for provider %s", provider.ID)
}
d.mu.Lock()
d.entries[provider.ID] = &metadataCacheEntry{
metadata: metadata,
expiresAt: time.Now().Add(1 * time.Hour),
}
d.mu.Unlock()
return metadata, nil
}
func (d *discovery) fetch(wellknownURL string) (*providerMetadata, error) {
res, err := d.httpClient.Get(wellknownURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
metadata := &providerMetadata{}
if err := json.Unmarshal(body, metadata); err != nil {
return nil, err
}
return metadata, nil
}
+199
View File
@@ -0,0 +1,199 @@
package oidc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
)
// jwk est une clé du document JWKS du fournisseur.
type jwk struct {
Kid string `json:"kid"`
Kty string `json:"kty"`
Alg string `json:"alg"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
Crv string `json:"crv"`
X string `json:"x"`
Y string `json:"y"`
}
type jwksDocument struct {
Keys []jwk `json:"keys"`
}
type jwksCacheEntry struct {
keys map[string]interface{}
expiresAt time.Time
}
// jwksStore maintient un cache des clés publiques de signature par fournisseur.
type jwksStore struct {
httpClient *http.Client
mu sync.Mutex
entries map[string]*jwksCacheEntry
}
func newJwksStore() *jwksStore {
return &jwksStore{
httpClient: &http.Client{Timeout: 10 * time.Second},
entries: make(map[string]*jwksCacheEntry),
}
}
// KeyFunc retourne une fonction de résolution de clé pour golang-jwt,
// valable pour un fournisseur donné.
func (s *jwksStore) KeyFunc(providerID string, jwksURI string) jwt.Keyfunc {
return func(t *jwt.Token) (interface{}, error) {
kid, ok := t.Header["kid"].(string)
if !ok || kid == "" {
return nil, fmt.Errorf("id token: kid manquant")
}
key, err := s.key(providerID, jwksURI, kid)
if err != nil {
return nil, err
}
return key, nil
}
}
// key retourne la clé publique correspondant au kid, avec mise en cache.
// Si le kid est inconnu, le cache est invalidé et le JWKS re-téléchargé
// (rotation de clés côté fournisseur).
func (s *jwksStore) key(providerID string, jwksURI string, kid string) (interface{}, error) {
fetch := func() (map[string]interface{}, error) {
keys, err := s.fetch(jwksURI)
if err != nil {
return nil, fmt.Errorf("error fetching provider JWKS: %s", err)
}
s.mu.Lock()
s.entries[providerID] = &jwksCacheEntry{
keys: keys,
expiresAt: time.Now().Add(1 * time.Hour),
}
s.mu.Unlock()
return keys, nil
}
s.mu.Lock()
entry, ok := s.entries[providerID]
valid := ok && time.Now().Before(entry.expiresAt)
s.mu.Unlock()
if valid {
if key, ok := entry.keys[kid]; ok {
return key, nil
}
}
keys, err := fetch()
if err != nil {
return nil, err
}
key, ok := keys[kid]
if !ok {
return nil, fmt.Errorf("id token: aucune clé JWKS ne correspond au kid %s", kid)
}
return key, nil
}
func (s *jwksStore) fetch(jwksURI string) (map[string]interface{}, error) {
res, err := s.httpClient.Get(jwksURI)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
document := &jwksDocument{}
if err := json.Unmarshal(body, document); err != nil {
return nil, err
}
keys := make(map[string]interface{})
for _, k := range document.Keys {
if k.Use != "" && k.Use != "sig" {
continue
}
switch k.Kty {
case "RSA":
key, err := rsaKey(k)
if err != nil {
return nil, err
}
keys[k.Kid] = key
case "EC":
key, err := ecKey(k)
if err != nil {
return nil, err
}
keys[k.Kid] = key
}
}
return keys, nil
}
func rsaKey(k jwk) (*rsa.PublicKey, error) {
if k.N == "" || k.E == "" {
return nil, fmt.Errorf("clé RSA JWKS incomplète (kid %s)", k.Kid)
}
nBytes, err := base64.RawURLEncoding.DecodeString(k.N)
if err != nil {
return nil, fmt.Errorf("module RSA invalide: %s", err)
}
eBytes, err := base64.RawURLEncoding.DecodeString(k.E)
if err != nil {
return nil, fmt.Errorf("exposant RSA invalide: %s", err)
}
return &rsa.PublicKey{
N: new(big.Int).SetBytes(nBytes),
E: int(new(big.Int).SetBytes(eBytes).Int64()),
}, nil
}
func ecKey(k jwk) (*ecdsa.PublicKey, error) {
if k.X == "" || k.Y == "" {
return nil, fmt.Errorf("clé EC JWKS incomplète (kid %s)", k.Kid)
}
var curve elliptic.Curve
switch strings.ToUpper(k.Crv) {
case "P-256":
curve = elliptic.P256()
case "P-384":
curve = elliptic.P384()
case "P-521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("courbe EC non supportée: %s", k.Crv)
}
xBytes, err := base64.RawURLEncoding.DecodeString(k.X)
if err != nil {
return nil, fmt.Errorf("coordonnée x invalide: %s", err)
}
yBytes, err := base64.RawURLEncoding.DecodeString(k.Y)
if err != nil {
return nil, fmt.Errorf("coordonnée y invalide: %s", err)
}
return &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(xBytes),
Y: new(big.Int).SetBytes(yBytes),
}, nil
}
+226
View File
@@ -0,0 +1,226 @@
package oidc
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
)
var (
ErrRegistrationUnsupported = errors.New("registration via OIDC is not supported: users are provisioned at login")
ErrNoEmail = errors.New("id token does not contain an email claim")
ErrUserDisabled = errors.New("user is disabled")
)
// tokenResponse est la réponse de l'endpoint token du fournisseur.
type tokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// idTokenClaims sont les claims du token ID émis par le fournisseur.
type idTokenClaims struct {
Email string `json:"email"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Picture string `json:"picture"`
jwt.RegisteredClaims
}
type OIDCAuthenticator struct {
tokenManager auth.TokenManager
oidcRepository domain.OIDCProviderRepository
userRepository domain.UserRepository
discovery *discovery
jwks *jwksStore
httpClient *http.Client
}
func NewOIDCAuthenticator(
tokenManager auth.TokenManager,
oidcRepository domain.OIDCProviderRepository,
userRepository domain.UserRepository,
) *OIDCAuthenticator {
return &OIDCAuthenticator{
tokenManager: tokenManager,
oidcRepository: oidcRepository,
userRepository: userRepository,
discovery: newDiscovery(),
jwks: newJwksStore(),
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
// AuthorizeURL construit l'URL d'autorisation vers laquelle rediriger l'utilisateur.
func (s *OIDCAuthenticator) AuthorizeURL(ctx context.Context, provider *domain.OIDCProvider, redirectURI string, state string) (string, error) {
metadata, err := s.discovery.Metadata(provider)
if err != nil {
return "", err
}
params := url.Values{}
params.Set("client_id", provider.ClientID)
params.Set("redirect_uri", redirectURI)
params.Set("response_type", "code")
params.Set("scope", "openid profile email")
params.Set("state", state)
return metadata.AuthorizationEndpoint + "?" + params.Encode(), nil
}
// ExchangeCode échange le code d'autorisation contre un token ID, le valide,
// puis délivre une session applicative.
func (s *OIDCAuthenticator) ExchangeCode(ctx context.Context, provider *domain.OIDCProvider, code string, redirectURI string) (*auth.Session, error) {
metadata, err := s.discovery.Metadata(provider)
if err != nil {
return nil, err
}
tokens, err := s.requestTokens(metadata.TokenEndpoint, provider, code, redirectURI)
if err != nil {
return nil, err
}
if tokens.IDToken == "" {
return nil, fmt.Errorf("provider %s did not return an id token", provider.ID)
}
claims, err := s.validateIDToken(provider, metadata, tokens.IDToken)
if err != nil {
return nil, err
}
if claims.Email == "" {
return nil, ErrNoEmail
}
user, err := s.provisionUser(claims)
if err != nil {
return nil, err
}
accessToken, expiresAt, err := s.tokenManager.GenerateAccessToken(user)
if err != nil {
return nil, fmt.Errorf("unable to generate access token: %s", err)
}
refreshToken, _, err := s.tokenManager.GenerateRefreshToken(user.ID)
if err != nil {
return nil, fmt.Errorf("unable to generate refresh token: %s", err)
}
return &auth.Session{
User: user,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
}, nil
}
// Authenticate implémente auth.Authenticator: le flux OIDC passe par le state
// signé qui transporte l'id du fournisseur, puis l'échange du code.
func (s *OIDCAuthenticator) Authenticate(ctx context.Context, creds auth.Credentials) (*auth.Session, error) {
providerID, err := s.tokenManager.ParseStateToken(creds.OIDCState)
if err != nil {
return nil, fmt.Errorf("invalid oidc state: %s", err)
}
provider, err := s.oidcRepository.FindOIDCProvider(providerID)
if err != nil {
return nil, fmt.Errorf("unknown oidc provider: %s", providerID)
}
return s.ExchangeCode(ctx, provider, creds.OIDCCode, creds.RedirectURI)
}
func (s *OIDCAuthenticator) Register(ctx context.Context, registration auth.Registration) (*auth.Session, error) {
return nil, ErrRegistrationUnsupported
}
func (s *OIDCAuthenticator) requestTokens(tokenEndpoint string, provider *domain.OIDCProvider, code string, redirectURI string) (*tokenResponse, error) {
form := url.Values{}
form.Set("grant_type", "authorization_code")
form.Set("code", code)
form.Set("redirect_uri", redirectURI)
form.Set("client_id", provider.ClientID)
form.Set("client_secret", provider.ClientSecret)
res, err := s.httpClient.Post(tokenEndpoint, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("error calling token endpoint: %s", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token endpoint returned status %d: %s", res.StatusCode, string(body))
}
tokens := &tokenResponse{}
if err := json.Unmarshal(body, tokens); err != nil {
return nil, err
}
return tokens, nil
}
func (s *OIDCAuthenticator) validateIDToken(provider *domain.OIDCProvider, metadata *providerMetadata, idToken string) (*idTokenClaims, error) {
claims := &idTokenClaims{}
_, err := jwt.ParseWithClaims(idToken, claims, s.jwks.KeyFunc(provider.ID, metadata.JwksURI),
jwt.WithValidMethods([]string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES521"}),
jwt.WithIssuer(metadata.Issuer),
jwt.WithAudience(provider.ClientID),
)
if err != nil {
return nil, fmt.Errorf("invalid id token: %s", err)
}
return claims, nil
}
func (s *OIDCAuthenticator) provisionUser(claims *idTokenClaims) (*domain.User, error) {
user, err := s.userRepository.FindUserByEmail(claims.Email)
if err == nil {
if !user.Enabled {
return nil, ErrUserDisabled
}
return user, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
firstname := claims.GivenName
if firstname == "" {
firstname = claims.PreferredUsername
}
user, err = s.userRepository.CreateUser(&domain.User{
Email: claims.Email,
Firstname: firstname,
Lastname: claims.FamilyName,
Enabled: true,
})
if err != nil {
return nil, fmt.Errorf("error creating oidc user %s: %s", claims.Email, err)
}
_, err = s.userRepository.CreateTeam(user.ID, &domain.Team{
Label: "Espace personnel",
DateCreated: time.Now(),
})
if err != nil {
return nil, fmt.Errorf("error creating personal team for oidc user %s: %s", claims.Email, err)
}
return user, nil
}
@@ -0,0 +1,277 @@
package oidc
import (
"context"
"crypto/rand"
"crypto/rsa"
"database/sql"
"encoding/base64"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type mockOIDCRepo struct {
domain.OIDCProviderRepository
provider *domain.OIDCProvider
}
func (m *mockOIDCRepo) FindOIDCProvider(id string) (*domain.OIDCProvider, error) {
return m.provider, nil
}
type mockUserRepo struct {
domain.UserRepository
created *domain.User
}
func (m *mockUserRepo) FindUserByEmail(email string) (*domain.User, error) {
return nil, sql.ErrNoRows
}
func (m *mockUserRepo) CreateUser(user *domain.User) (*domain.User, error) {
user.ID = "user-1"
m.created = user
return user, nil
}
func (m *mockUserRepo) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) {
return team, nil
}
// fakeIdP simule un fournisseur OpenID Connect (Keycloak-like):
// document de découverte, JWKS et endpoint token.
type fakeIdP struct {
server *httptest.Server
key *rsa.PrivateKey
kid string
idToken string
provider *domain.OIDCProvider
redirectURI string
tokenCalls int
}
func newFakeIdP(t *testing.T) *fakeIdP {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
idp := &fakeIdP{key: key, kid: "test-key-1", redirectURI: "http://localhost:3000/api/auth/oidc/callback"}
mux := http.NewServeMux()
server := httptest.NewServer(mux)
idp.server = server
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"issuer": server.URL,
"authorization_endpoint": server.URL + "/authorize",
"token_endpoint": server.URL + "/token",
"jwks_uri": server.URL + "/jwks",
})
})
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string][]map[string]string{
"keys": {{
"kid": idp.kid,
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": base64.RawURLEncoding.EncodeToString(key.PublicKey.N.Bytes()),
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.PublicKey.E)).Bytes()),
}},
})
})
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
idp.tokenCalls++
if err := r.ParseForm(); err != nil {
t.Errorf("invalid token request form: %s", err)
}
if r.Form.Get("grant_type") != "authorization_code" {
t.Errorf("unexpected grant_type: %s", r.Form.Get("grant_type"))
}
if r.Form.Get("redirect_uri") != idp.redirectURI {
t.Errorf("unexpected redirect_uri: %s", r.Form.Get("redirect_uri"))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"access_token": "opaque-access-token",
"id_token": idp.idToken,
"token_type": "Bearer",
"expires_in": 300,
})
})
idp.provider = &domain.OIDCProvider{
ID: "kc",
Label: "Keycloak",
ClientID: "lassistanoque",
ClientSecret: "secret",
WellknownURL: server.URL + "/.well-known/openid-configuration",
}
return idp
}
func (idp *fakeIdP) signIDToken(t *testing.T, claims jwt.MapClaims) string {
t.Helper()
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = idp.kid
signed, err := token.SignedString(idp.key)
if err != nil {
t.Fatal(err)
}
return signed
}
func TestMain(m *testing.M) {
os.Setenv("LASSISTANOQUE_JWT_SECRET", "test-secret")
os.Exit(m.Run())
}
func newAuthenticator(idp *fakeIdP) *OIDCAuthenticator {
tokenManager := security.NewJwtTokenManager(time.Hour, 24*time.Hour, "lassistanoque")
return NewOIDCAuthenticator(tokenManager, &mockOIDCRepo{provider: idp.provider}, &mockUserRepo{})
}
func TestAuthorizeURL(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
authenticator := newAuthenticator(idp)
state, err := authenticator.tokenManager.GenerateStateToken(idp.provider.ID)
if err != nil {
t.Fatal(err)
}
url, err := authenticator.AuthorizeURL(context.Background(), idp.provider, idp.redirectURI, state)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(url, idp.server.URL+"/authorize?") {
t.Errorf("unexpected authorize url: %s", url)
}
for _, expected := range []string{
"client_id=lassistanoque",
"response_type=code",
"scope=openid+profile+email",
"state=",
} {
if !strings.Contains(url, expected) {
t.Errorf("authorize url misses %s: %s", expected, url)
}
}
}
func TestExchangeCodeCreatesUserAndSession(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
idp.idToken = idp.signIDToken(t, jwt.MapClaims{
"iss": idp.server.URL,
"aud": idp.provider.ClientID,
"exp": time.Now().Add(5 * time.Minute).Unix(),
"email": "fabien@example.com",
"given_name": "Fabien",
"family_name": "Dupont",
})
userRepo := &mockUserRepo{}
tokenManager := security.NewJwtTokenManager(time.Hour, 24*time.Hour, "lassistanoque")
authenticator := NewOIDCAuthenticator(tokenManager, &mockOIDCRepo{provider: idp.provider}, userRepo)
session, err := authenticator.ExchangeCode(context.Background(), idp.provider, "auth-code", idp.redirectURI)
if err != nil {
t.Fatal(err)
}
if idp.tokenCalls != 1 {
t.Errorf("expected 1 token endpoint call, got %d", idp.tokenCalls)
}
if userRepo.created == nil {
t.Fatal("user was not provisioned")
}
if userRepo.created.Email != "fabien@example.com" ||
userRepo.created.Firstname != "Fabien" ||
userRepo.created.Lastname != "Dupont" {
t.Errorf("provisioned user mismatch: %+v", userRepo.created)
}
if !userRepo.created.Enabled {
t.Error("provisioned user should be enabled")
}
if session.AccessToken == "" || session.RefreshToken == "" {
t.Error("session tokens are empty")
}
if session.User == nil || session.User.Email != "fabien@example.com" {
t.Errorf("session user mismatch: %+v", session.User)
}
// le token applicatif doit être valide
user, err := tokenManager.ParseAndValidate(session.AccessToken)
if err != nil {
t.Fatalf("generated access token invalid: %s", err)
}
if user.ID != "user-1" {
t.Errorf("unexpected user id in access token: %s", user.ID)
}
}
func TestExchangeCodeRejectsForgedIDToken(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
// token signé par une autre clé que celle du JWKS
forgedKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iss": idp.server.URL,
"aud": idp.provider.ClientID,
"exp": time.Now().Add(5 * time.Minute).Unix(),
"email": "attacker@example.com",
})
token.Header["kid"] = idp.kid
idp.idToken, err = token.SignedString(forgedKey)
if err != nil {
t.Fatal(err)
}
authenticator := newAuthenticator(idp)
if _, err := authenticator.ExchangeCode(context.Background(), idp.provider, "auth-code", idp.redirectURI); err == nil {
t.Fatal("exchange should fail with a forged id token")
}
}
func TestExchangeCodeRejectsWrongIssuer(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
idp.idToken = idp.signIDToken(t, jwt.MapClaims{
"iss": "https://evil.example.com",
"aud": idp.provider.ClientID,
"exp": time.Now().Add(5 * time.Minute).Unix(),
"email": "attacker@example.com",
})
authenticator := newAuthenticator(idp)
if _, err := authenticator.ExchangeCode(context.Background(), idp.provider, "auth-code", idp.redirectURI); err == nil {
t.Fatal("exchange should fail with a wrong issuer")
}
}
@@ -11,8 +11,13 @@ import (
type Database interface {
SettingsRepository() domain.SettingsRepository
UserRepository() domain.UserRepository
OIDCProviderRepository() domain.OIDCProviderRepository
FileRepository() domain.FileRepository
ProviderRepository() domain.ProviderRepository
AgentRepository() domain.AgentRepository
ChatRepository() domain.ChatRepository
ToolRepository() domain.ToolRepository
TaskRepository() domain.TaskRepository
Migrate() error
Close()
}
@@ -0,0 +1,84 @@
package orm
import (
"fmt"
"strings"
)
func buildInsertSql(tablename string, fields []dbfield, dialect Dialect) string {
cols := make([]string, len(fields))
placeholders := make([]string, len(fields))
for i, f := range fields {
cols[i] = f.dbname
placeholders[i] = fmt.Sprintf("$%d", i+1)
}
return fmt.Sprintf(
"insert into %s (%s) values (%s)",
tablename,
strings.Join(cols, ","),
strings.Join(placeholders, ","),
)
}
func buildUpdateSql(tablename string, fields []dbfield, dialect Dialect) string {
predicates := make([]string, len(fields)-1)
var where string
i := 0
for _, f := range fields {
if f.dbname == "id" {
where = fmt.Sprintf("id=$%d and _version=$%d", len(fields), len(fields)+1)
} else {
predicates[i] = fmt.Sprintf("%s=$%d", f.dbname, i+1)
i++
}
}
return fmt.Sprintf(
"update %s set %s where %s",
tablename,
strings.Join(predicates, ","),
where,
)
}
func buildSelectbyidSql(tablename string, fields []dbfield, dialect Dialect) string {
columns := make([]string, len(fields))
var where string
for i, f := range fields {
columns[i] = fmt.Sprintf("%s", f.dbname)
}
where = "id=$1"
return fmt.Sprintf(
"select %s from %s where %s",
strings.Join(columns, ","),
tablename,
where,
)
}
func buildDeletebyidSql(tablename string, fields []dbfield, dialect Dialect) string {
where := "id=$1"
return fmt.Sprintf(
"delete from %s where %s",
tablename,
where,
)
}
func buildSelectWhere(tablename string, fields []dbfield, dialect Dialect) string {
columns := make([]string, len(fields))
for i, f := range fields {
columns[i] = fmt.Sprintf("%s", f.dbname)
}
return fmt.Sprintf(
"select %s from %s",
strings.Join(columns, ","),
tablename,
)
}
func buildDeleteWhere(tablename string, fields []dbfield, dialect Dialect) string {
return fmt.Sprintf(
"delete from %s",
tablename,
)
}
@@ -0,0 +1,289 @@
package orm
import (
"database/sql"
"fmt"
"reflect"
"strings"
"time"
)
type SqliteDialect struct{}
func NewSqliteDialect() *SqliteDialect {
return &SqliteDialect{}
}
func convertType(f dbfield) string {
switch f.stype {
case "string", "*string":
return "text"
case "bool", "int", "*int", "time.Time", "*time.Time":
return "numeric"
case "[]byte":
return "blob"
}
return "text"
}
func defaultValue(f dbfield) string {
if f.dbname == "_date_created" {
return "current_timestamp"
}
switch f.stype {
case "string":
return "''"
case "bool", "int", "time.Time":
return "0"
}
return "''"
}
func (d *SqliteDialect) TableExists(db *sql.DB, tableName string) (bool, error) {
sql := "select name from sqlite_schema where type='table' and name=$1"
rows, err := db.Query(sql, tableName)
if err != nil {
return false, err
}
if rows.Next() {
rows.Close()
return true, nil
}
return false, nil
}
func (d *SqliteDialect) ColumnSpec(f dbfield) (string, error) {
if f.dbname == "id" {
return "id text not null primary key", nil
}
if strings.HasPrefix(f.stype, "*") || strings.HasPrefix(f.stype, "[]") {
return fmt.Sprintf("%s %s", f.dbname, convertType(f)), nil
}
return fmt.Sprintf("%s %s not null default %s", f.dbname, convertType(f), defaultValue(f)), nil
}
// ScanResult scans a single row into the provided fields slice.
// For SQLite, this handles time.Time fields which are returned as strings.
func (d *SqliteDialect) ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error {
// SQLite returns time.Time as strings, so we need special handling
// Check if any field is a time type
hasTimeField := false
for _, f := range fieldTypes {
if f.stype == "time.Time" || f.stype == "*time.Time" {
hasTimeField = true
break
}
}
if !hasTimeField {
// No time fields, use standard scan
return row.Scan(fields...)
}
// We have time fields - need custom handling
// Create temp destinations for all fields
tempFields := make([]any, len(fields))
fieldInfos := make([]struct {
index int
dest any
isTime bool
isTimePtr bool
}, len(fields))
for i, f := range fieldTypes {
isTime := f.stype == "time.Time"
isTimePtr := f.stype == "*time.Time"
fieldInfos[i] = struct {
index int
dest any
isTime bool
isTimePtr bool
}{
index: i,
dest: fields[i],
isTime: isTime,
isTimePtr: isTimePtr,
}
if isTime || isTimePtr {
var s sql.NullString
tempFields[i] = &s
} else {
tempFields[i] = fields[i]
}
}
if err := row.Scan(tempFields...); err != nil {
return err
}
// Copy time values back to original destinations
for _, info := range fieldInfos {
if info.isTime || info.isTimePtr {
s := tempFields[info.index].(*sql.NullString)
if s.Valid {
// Try to parse the string as time
t, err := tryParseTime(s.String)
if err != nil {
return fmt.Errorf("failed to parse time field: %w", err)
}
// Set the value in the original destination
destVal := reflect.ValueOf(info.dest).Elem()
if info.isTime {
// dest is *time.Time, set the value
destVal.Set(reflect.ValueOf(t))
} else if info.isTimePtr {
// dest is **time.Time, allocate and set
ptr := reflect.New(destVal.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(t))
destVal.Set(ptr)
}
}
}
}
return nil
}
// ScanRows scans multiple rows using the dialect-specific logic
func (d *SqliteDialect) ScanRows(rows *sql.Rows, fieldDest func() []any, fieldTypes []dbfield) ([]any, error) {
// Check if any field is a time type
hasTimeField := false
for _, f := range fieldTypes {
if f.stype == "time.Time" || f.stype == "*time.Time" {
hasTimeField = true
break
}
}
if !hasTimeField {
// No time fields, use standard scan
var results []any
for rows.Next() {
fields := fieldDest()
if err := rows.Scan(fields...); err != nil {
return nil, err
}
results = append(results, fields...)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
// We have time fields - need custom handling
var results []any
for rows.Next() {
fields := fieldDest()
// Create temp destinations for all fields
tempFields := make([]any, len(fields))
fieldInfos := make([]struct {
index int
dest any
isTime bool
isTimePtr bool
}, len(fields))
for i, f := range fieldTypes {
isTime := f.stype == "time.Time"
isTimePtr := f.stype == "*time.Time"
fieldInfos[i] = struct {
index int
dest any
isTime bool
isTimePtr bool
}{
index: i,
dest: fields[i],
isTime: isTime,
isTimePtr: isTimePtr,
}
if isTime || isTimePtr {
var s sql.NullString
tempFields[i] = &s
} else {
tempFields[i] = fields[i]
}
}
if err := rows.Scan(tempFields...); err != nil {
return nil, err
}
// Copy time values back to original destinations
for _, info := range fieldInfos {
if info.isTime || info.isTimePtr {
s := tempFields[info.index].(*sql.NullString)
if s.Valid {
t, err := tryParseTime(s.String)
if err != nil {
return nil, fmt.Errorf("failed to parse time field: %w", err)
}
// Set the value in the original destination
destVal := reflect.ValueOf(info.dest).Elem()
if info.isTime {
// dest is *time.Time, set the value
destVal.Set(reflect.ValueOf(t))
} else if info.isTimePtr {
// dest is **time.Time, allocate and set
ptr := reflect.New(destVal.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(t))
destVal.Set(ptr)
}
}
}
}
results = append(results, fields...)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
// tryParseTime attempts to parse a time string in various common formats
// SQLite driver returns time as "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000"
// We need to strip the monotonic clock part (m=...) before parsing
func tryParseTime(s string) (time.Time, error) {
// Remove monotonic clock part if present
// Format: "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000"
if idx := strings.Index(s, " m="); idx != -1 {
s = s[:idx]
}
// Try common formats
formats := []string{
time.RFC3339,
time.RFC3339Nano,
"2006-01-02T15:04:05Z07:00",
"2006-01-02 15:04:05.999999999-07:00",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05+00:00",
// Go's default time.String() format (without monotonic clock)
"2006-01-02 15:04:05.999999999 -0700 MST",
// SQLite without timezone
"2006-01-02 15:04:05.999999",
}
var err error
var t time.Time
for _, format := range formats {
t, err = time.Parse(format, s)
if err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time: %s", s)
}
@@ -0,0 +1,376 @@
package orm
import (
"database/sql"
"fmt"
"reflect"
"strings"
"time"
)
type dbfield struct {
name string
stype string
dbname string
}
type Dialect interface {
TableExists(db *sql.DB, tableName string) (bool, error)
ColumnSpec(f dbfield) (string, error)
ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error
}
type Table[T any] struct {
db *sql.DB
dialect Dialect
tablename string
fields []dbfield
insertsql string
updatesql string
selectbyid string
deletebyid string
selectwhere string
deletewhere string
}
func NewTable[T any](db *sql.DB, dialect Dialect, tablename string) (Table[T], error) {
var instance T
t := reflect.TypeOf(instance)
fields := make([]dbfield, 0)
for field := range t.Fields() {
if field.Tag.Get("db") != "-" {
f := dbfield{
name: field.Name,
stype: field.Type.String(),
dbname: field.Tag.Get("db"),
}
fields = append(fields, f)
}
}
return Table[T]{
db: db,
dialect: dialect,
tablename: tablename,
fields: fields,
insertsql: buildInsertSql(tablename, fields, dialect),
updatesql: buildUpdateSql(tablename, fields, dialect),
selectbyid: buildSelectbyidSql(tablename, fields, dialect),
deletebyid: buildDeletebyidSql(tablename, fields, dialect),
selectwhere: buildSelectWhere(tablename, fields, dialect),
deletewhere: buildDeleteWhere(tablename, fields, dialect),
}, nil
}
func (t Table[T]) Sync() error {
if exists, err := t.dialect.TableExists(t.db, t.tablename); exists {
return nil
} else if err != nil {
return err
}
columns := make([]string, len(t.fields))
for i, f := range t.fields {
c, err := t.dialect.ColumnSpec(f)
if err != nil {
return err
}
columns[i] = c
}
sql := fmt.Sprintf("create table %s (%s)", t.tablename, strings.Join(columns, ","))
_, err := t.db.Exec(sql)
if err != nil {
return err
}
return nil
}
func (t Table[T]) Get(id string) (*T, error) {
row := t.db.QueryRow(t.selectbyid, id)
if row == nil {
return nil, sql.ErrNoRows
}
var obj T
// Create field destinations
ptrVal := reflect.ValueOf(&obj).Elem()
fields := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := ptrVal.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
fields[i] = fieldValue.Addr().Interface()
}
// Use dialect to scan with proper type handling
if err := t.dialect.ScanResult(row, fields, t.fields); err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, err
}
return &obj, nil
}
func (t Table[T]) SelectOne(where string, args ...any) (*T, error) {
query := t.selectwhere + " where " + where + " limit 1"
row := t.db.QueryRow(query, args...)
if row == nil {
return nil, sql.ErrNoRows
}
var obj T
ptrVal := reflect.ValueOf(&obj).Elem()
fields := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := ptrVal.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
fields[i] = fieldValue.Addr().Interface()
}
if err := t.dialect.ScanResult(row, fields, t.fields); err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, err
}
return &obj, nil
}
type query struct {
where string
args []any
orderby string
pagenum int
pagesize int
}
type Option func(*query)
func WithWhere(where string, args ...any) Option {
return func(q *query) {
q.where = where
q.args = args
}
}
func WithOrder(orderby string) Option {
return func(q *query) {
q.orderby = orderby
}
}
func WithPagination(pagenum, pagesize int) Option {
return func(q *query) {
q.pagenum = pagenum
q.pagesize = pagesize
}
}
func (t Table[T]) Select(options ...Option) ([]*T, error) {
q := &query{}
for _, o := range options {
o(q)
}
query := t.selectwhere
if q.where != "" {
query += " where " + q.where
}
if q.orderby != "" {
query += " order by " + q.orderby
}
if q.pagesize != 0 {
query += fmt.Sprintf(" limit %d", q.pagesize)
if q.pagenum > 0 {
// pages en base 0 : la page 0 est la première page
query += fmt.Sprintf(" offset %d", q.pagesize*q.pagenum)
}
}
var err error
var rows *sql.Rows
if q.args == nil {
rows, err = t.db.Query(query)
} else {
rows, err = t.db.Query(query, q.args...)
}
if err != nil {
return nil, err
}
defer rows.Close()
var results []*T
for rows.Next() {
var obj T
ptrVal := reflect.ValueOf(&obj).Elem()
fields := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := ptrVal.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
fields[i] = fieldValue.Addr().Interface()
}
// Use dialect to scan with proper type handling
// *sql.Rows implements Scanner interface
if err := t.dialect.ScanResult(rows, fields, t.fields); err != nil {
return nil, err
}
results = append(results, &obj)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
func (t Table[T]) Delete(id string) error {
_, err := t.db.Exec(t.deletebyid, id)
return err
}
func (t Table[T]) DeleteWhere(where string, args ...any) error {
query := t.deletewhere
if where != "" {
query += " where " + where
}
_, err := t.db.Exec(query, args...)
return err
}
func (t Table[T]) Insert(value *T) (*T, error) {
// Get a settable reflect value of the struct
// value is *T, &value is **T, .Elem() gives *T (settables), .Elem() gives T (settables)
val := reflect.ValueOf(&value).Elem().Elem()
// Auto-fill ID field if it exists and is empty
if fieldID := val.FieldByName("ID"); fieldID.IsValid() && fieldID.CanSet() && fieldID.Kind() == reflect.String {
if fieldID.String() == "" {
fieldID.SetString(GenID())
}
}
// Auto-fill VersionId field if it exists and is empty
if fieldVersion := val.FieldByName("VersionId"); fieldVersion.IsValid() && fieldVersion.CanSet() && fieldVersion.Kind() == reflect.String {
if fieldVersion.String() == "" {
fieldVersion.SetString(GenID())
}
}
// Auto-fill DateCreated or DateCreate field if it exists
now := time.Now()
if fieldCreate := val.FieldByName("DateCreated"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) {
fieldCreate.Set(reflect.ValueOf(now))
} else if fieldCreate := val.FieldByName("DateCreate"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) {
fieldCreate.Set(reflect.ValueOf(now))
}
vals := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := val.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
vals[i] = fieldValue.Interface()
}
_, err := t.db.Exec(t.insertsql, vals...)
if err != nil {
return nil, err
}
return value, nil
}
func (t Table[T]) Update(value *T) (*T, error) {
// Get a settable reflect value of the struct
val := reflect.ValueOf(&value).Elem().Elem()
// Save old VersionId for WHERE clause
versionField := val.FieldByName("VersionId")
if !versionField.IsValid() {
return nil, fmt.Errorf("VersionId field not found")
}
oldVersionId := versionField.String()
// Generate new VersionId for optimistic locking
newVersionId := GenID()
versionField.SetString(newVersionId)
// Update DateUpdated field if it exists
if fieldUpdated := val.FieldByName("DateUpdated"); fieldUpdated.IsValid() && fieldUpdated.CanSet() {
if fieldUpdated.Kind() == reflect.Ptr {
now := time.Now()
if fieldUpdated.IsNil() {
fieldUpdated.Set(reflect.New(fieldUpdated.Type().Elem()))
}
fieldUpdated.Elem().Set(reflect.ValueOf(now))
} else if fieldUpdated.Type() == reflect.TypeOf(time.Time{}) {
fieldUpdated.Set(reflect.ValueOf(time.Now()))
}
}
// Build the values slice for the prepared statement
// UPDATE SQL expects: SET fields (excluding ID), then WHERE: ID and VersionId (old version)
vals := make([]any, 0, len(t.fields))
// Add all fields except ID for SET clause (includes new VersionId)
for _, f := range t.fields {
if f.dbname == "id" {
continue // Skip ID field for SET clause
}
fieldValue := val.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
vals = append(vals, fieldValue.Interface())
}
// Add ID and OLD VersionId at the end for WHERE clause
idField := val.FieldByName("ID")
if !idField.IsValid() {
return nil, fmt.Errorf("ID field not found")
}
vals = append(vals, idField.Interface(), oldVersionId)
result, err := t.db.Exec(t.updatesql, vals...)
if err != nil {
return nil, err
}
// Check if any row was affected
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, fmt.Errorf("no rows affected: record not found or version mismatch")
}
return value, nil
}
func (t Table[T]) Debug() {
fmt.Println("insert: ", t.insertsql)
fmt.Println("update: ", t.updatesql)
fmt.Println("selectbyid: ", t.selectbyid)
fmt.Println("deletebyid: ", t.deletebyid)
fmt.Println("selectwhere: ", t.selectwhere)
fmt.Println("deletewhere: ", t.deletewhere)
}
@@ -0,0 +1,21 @@
package orm
import (
"crypto/rand"
"encoding/hex"
"github.com/sixafter/nanoid"
)
func GenID() string {
id, err := nanoid.New()
if err != nil {
// Fallback to random hex
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "fallback-id"
}
return hex.EncodeToString(b)
}
return id.String()
}
@@ -0,0 +1,63 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoAgentRepository struct {
db *sql.DB
agentTable orm.Table[domain.Agent]
}
func NewTursoAgentRepository(db *sql.DB) *TursoAgentRepository {
dialect := orm.NewSqliteDialect()
agentTable, err := orm.NewTable[domain.Agent](db, dialect, "agents")
if err != nil {
log.Fatalf("error creating lasebuche agent table")
}
return &TursoAgentRepository{
db: db,
agentTable: agentTable,
}
}
func (r *TursoAgentRepository) ListAgents(userID string, teamID string) ([]*domain.Agent, error) {
return r.agentTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
)
}
func (r *TursoAgentRepository) GetAgent(userID string, teamID string, id string) (*domain.Agent, error) {
return r.agentTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoAgentRepository) CreateAgent(userID string, agent *domain.Agent) (*domain.Agent, error) {
return r.agentTable.Insert(agent)
}
func (r *TursoAgentRepository) UpdateAgent(userID string, agent *domain.Agent) (*domain.Agent, error) {
return r.agentTable.Update(agent)
}
func (r *TursoAgentRepository) DeleteAgent(userID string, teamID string, id string) error {
return r.agentTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
@@ -0,0 +1,140 @@
package turso
import (
"database/sql"
"fmt"
"log"
"strings"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoChatRepository struct {
db *sql.DB
chatTable orm.Table[domain.Chat]
messageTable orm.Table[domain.Message]
}
func NewTursoChatRepository(db *sql.DB) *TursoChatRepository {
dialect := orm.NewSqliteDialect()
chatTable, err := orm.NewTable[domain.Chat](db, dialect, "chats")
messageTable, err := orm.NewTable[domain.Message](db, dialect, "chat_messages")
if err != nil {
log.Fatalf("error creating lasebuche agent table")
}
return &TursoChatRepository{
db: db,
chatTable: chatTable,
messageTable: messageTable,
}
}
func (r *TursoChatRepository) ListChats(userID string, teamID string, page int, query string, taskID 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+"%")
}
if taskID != "" {
where += fmt.Sprintf(" and task_id=$%d", len(args)+1)
args = append(args, taskID)
}
return r.chatTable.Select(
orm.WithWhere(where, args...),
orm.WithOrder("_date_created desc"),
orm.WithPagination(page, 20),
)
}
func (r *TursoChatRepository) ListChatsWithUnfreshTitles(teamID string) ([]*domain.Chat, error) {
return r.chatTable.Select(
orm.WithWhere(
"team_id=$1 and fresh_title=0",
teamID,
),
)
}
func (r *TursoChatRepository) GetChat(userID string, teamID string, id string) (*domain.Chat, error) {
return r.chatTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoChatRepository) CreateChat(userID string, chat *domain.Chat) (*domain.Chat, error) {
return r.chatTable.Insert(chat)
}
func (r *TursoChatRepository) UpdateChat(userID string, chat *domain.Chat) (*domain.Chat, error) {
return r.chatTable.Update(chat)
}
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,
teamID,
userID,
)
}
func (r *TursoChatRepository) GetChatMessages(userID string, teamID string, chatID string, withToolCallResponses bool) ([]*domain.Message, error) {
var messages []*domain.Message
var err error
if !withToolCallResponses {
messages, err = r.messageTable.Select(
orm.WithWhere(
`chat_id=$1
and team_id=$2
and team_id in (select team_id from user_teams where user_id=$3)
and (tool_call_id='' or tool_call_id is null)`,
chatID,
teamID,
userID,
),
orm.WithOrder("_date_created asc"),
)
} else {
messages, err = r.messageTable.Select(
orm.WithWhere(
"chat_id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
chatID,
teamID,
userID,
),
orm.WithOrder("_date_created asc"),
)
}
if err != nil {
return nil, err
}
for _, m := range messages {
m.Json2toolCall()
}
return messages, nil
}
func (r *TursoChatRepository) CreateChatMessage(userID string, message *domain.Message) (*domain.Message, error) {
message.ToolCall2Json()
return r.messageTable.Insert(message)
}
+22 -2
View File
@@ -24,7 +24,7 @@ type TursoDB struct {
func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
db, err := sql.Open("turso", cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err)
return nil, fmt.Errorf("error connecting to db %s: %s", cfg.DatabaseURL, err)
}
return &TursoDB{
DB: db,
@@ -64,10 +64,30 @@ func (db *TursoDB) SettingsRepository() domain.SettingsRepository {
return NewTursoSettingsRepository(db.DB)
}
func (db *TursoDB) OIDCProviderRepository() domain.OIDCProviderRepository {
return NewTursoOIDCProviderRepository(db.DB)
}
func (db *TursoDB) FileRepository() domain.FileRepository {
return NewTursoFileRepository(db.DB)
}
func (db *TursoDB) ProviderRepository() domain.ProviderRepository {
return NewTursoModelRepository(db.DB)
return NewTursoProviderRepository(db.DB)
}
func (db *TursoDB) AgentRepository() domain.AgentRepository {
return NewTursoAgentRepository(db.DB)
}
func (db *TursoDB) ChatRepository() domain.ChatRepository {
return NewTursoChatRepository(db.DB)
}
func (db *TursoDB) ToolRepository() domain.ToolRepository {
return NewTursoToolRepository(db.DB)
}
func (db *TursoDB) TaskRepository() domain.TaskRepository {
return NewTursoTaskRepository(db.DB)
}
@@ -6,19 +6,19 @@ import (
"log"
"time"
"gitea.trankilou.fr/fabien/lasebuche"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
)
type TursoFileRepository struct {
db *sql.DB
FileTable lasebuche.Table[domain.File]
FileTable orm.Table[domain.File]
}
func NewTursoFileRepository(db *sql.DB) *TursoFileRepository {
dialect := lasebuche.NewSqliteDialect()
fileTable, err := lasebuche.NewTable[domain.File](db, dialect, "settings")
dialect := orm.NewSqliteDialect()
fileTable, err := orm.NewTable[domain.File](db, dialect, "settings")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
@@ -0,0 +1,109 @@
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','','task-9','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))
}
// filtre par tâche
taskRes, err := repo.ListChats("u1", "t1", 0, "", "task-9")
if err != nil {
t.Fatal(err)
}
if len(taskRes) != 1 || taskRes[0].ID != "c2" {
t.Fatalf("filter task-9: expected [c2], got %v", taskRes)
}
_ = domain.Chat{}
}
@@ -9,5 +9,7 @@ drop table user_teams;
drop table channels;
drop table tools;
drop table tasks;
drop table history;
drop table chats;
drop table chat_contents;
drop table files;
drop table agents;
@@ -63,7 +63,7 @@ create table user_addresses (
create table teams (
id text not null primary key,
label text not null,
default_model_id text,
default_agent_id text,
_date_created numeric not null default current_timestamp,
_date_updated numeric ,
_version text not null
@@ -95,9 +95,8 @@ create table tools (
id text not null primary key,
team_id text not null,
name text not null,
type text not null,
enabled numeric not null default 1,
configuration text not null default '{}',
configuration text not null default '',
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
@@ -117,14 +116,31 @@ create table tasks (
_version text not null
);
create table history (
create table chats (
id text not null primary key,
task_id text not null,
start_datetime numeric,
team_id text not null,
start_datetime numeric not null,
end_datetime numeric,
prompt text not null,
log text not null,
response text not null,
agent_id text,
user_id text,
task_id text,
title text,
fresh_title numeric default 0,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
create table chat_messages (
id text not null primary key,
team_id text not null,
chat_id text not null,
content_datetime numeric,
role text not null,
content text,
agent_id text,
tool_call_id text,
tool_calls text,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
@@ -141,3 +157,24 @@ create table files (
_date_updated numeric ,
_version text not null
);
create table agents (
id text not null primary key,
team_id text not null,
name text not null default '',
system_prompt text not null default '',
def_provider_id text not null default '',
def_model_id text not null default '',
tools_policy text not null default '',
sub_agents numeric not null default 0,
loop_strategy text not null default '',
max_iterations numeric not null default 1,
stopping_criteria text not null default '',
-- kb text not null default '',
-- tools text not null default '',
-- skills text not null default '',
-- channels text not null default '',
_date_created numeric not null default current_timestamp,
_date_updated numeric ,
_version text not null
);
@@ -0,0 +1 @@
alter table tasks drop column repeat;
@@ -0,0 +1 @@
alter table tasks add column repeat integer not null default 1;
@@ -0,0 +1 @@
alter table tasks drop column last_datetime;
@@ -0,0 +1 @@
alter table tasks add column last_datetime numeric;
@@ -0,0 +1,34 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoOIDCProviderRepository struct {
db *sql.DB
OIDCTable orm.Table[domain.OIDCProvider]
}
func NewTursoOIDCProviderRepository(db *sql.DB) *TursoOIDCProviderRepository {
dialect := orm.NewSqliteDialect()
oidcTable, err := orm.NewTable[domain.OIDCProvider](db, dialect, "oidc")
if err != nil {
log.Fatalf("error creating oidc provider table")
}
return &TursoOIDCProviderRepository{
db: db,
OIDCTable: oidcTable,
}
}
func (or *TursoOIDCProviderRepository) ListOIDCProviders() ([]*domain.OIDCProvider, error) {
return or.OIDCTable.Select(orm.WithOrder("label asc"))
}
func (or *TursoOIDCProviderRepository) FindOIDCProvider(id string) (*domain.OIDCProvider, error) {
return or.OIDCTable.Get(id)
}
@@ -4,37 +4,40 @@ import (
"database/sql"
"log"
"gitea.trankilou.fr/fabien/lasebuche"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoModelRepository struct {
type TursoProviderRepository struct {
db *sql.DB
providerTable lasebuche.Table[domain.Provider]
providerTable orm.Table[domain.Provider]
}
func NewTursoModelRepository(db *sql.DB) *TursoModelRepository {
dialect := lasebuche.NewSqliteDialect()
providerTable, err := lasebuche.NewTable[domain.Provider](db, dialect, "providers")
func NewTursoProviderRepository(db *sql.DB) *TursoProviderRepository {
dialect := orm.NewSqliteDialect()
providerTable, err := orm.NewTable[domain.Provider](db, dialect, "providers")
if err != nil {
log.Fatalf("error creating lasebuche team table")
log.Fatalf("error creating lasebuche provider table")
}
return &TursoModelRepository{
return &TursoProviderRepository{
db: db,
providerTable: providerTable,
}
}
func (r *TursoModelRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
return r.providerTable.SelectWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
func (r *TursoProviderRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
return r.providerTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
orm.WithOrder("name"),
)
}
func (r *TursoModelRepository) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) {
func (r *TursoProviderRepository) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) {
return r.providerTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
@@ -43,15 +46,15 @@ func (r *TursoModelRepository) GetProvider(userID string, teamID string, id stri
)
}
func (r *TursoModelRepository) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
func (r *TursoProviderRepository) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
return r.providerTable.Insert(provider)
}
func (r *TursoModelRepository) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
func (r *TursoProviderRepository) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
return r.providerTable.Update(provider)
}
func (r *TursoModelRepository) DeleteProvider(userID string, teamID string, id string) error {
func (r *TursoProviderRepository) DeleteProvider(userID string, teamID string, id string) error {
return r.providerTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
@@ -4,18 +4,18 @@ import (
"database/sql"
"log"
"gitea.trankilou.fr/fabien/lasebuche"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoSettingsRepository struct {
db *sql.DB
SettingsTable lasebuche.Table[domain.Settings]
SettingsTable orm.Table[domain.Settings]
}
func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository {
dialect := lasebuche.NewSqliteDialect()
settingsTable, err := lasebuche.NewTable[domain.Settings](db, dialect, "settings")
dialect := orm.NewSqliteDialect()
settingsTable, err := orm.NewTable[domain.Settings](db, dialect, "settings")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
@@ -0,0 +1,85 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoTaskRepository struct {
db *sql.DB
taskTable orm.Table[domain.Task]
}
func NewTursoTaskRepository(db *sql.DB) *TursoTaskRepository {
dialect := orm.NewSqliteDialect()
taskTable, err := orm.NewTable[domain.Task](db, dialect, "tasks")
if err != nil {
log.Fatalf("error creating task table")
}
return &TursoTaskRepository{
db: db,
taskTable: taskTable,
}
}
func (r *TursoTaskRepository) ListTasks(userID string, teamID string) ([]*domain.Task, error) {
return r.taskTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
orm.WithOrder("_date_created desc"),
)
}
func (r *TursoTaskRepository) ListActiveTasks() ([]*domain.Task, error) {
return r.taskTable.Select(
orm.WithWhere("status=$1", domain.TaskStatusActive),
)
}
func (r *TursoTaskRepository) GetTaskByID(userID string, teamID string, id string) (*domain.Task, error) {
return r.taskTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoTaskRepository) CreateTask(task *domain.Task) (*domain.Task, error) {
return r.taskTable.Insert(task)
}
func (r *TursoTaskRepository) UpdateTask(task *domain.Task) (*domain.Task, error) {
return r.taskTable.Update(task)
}
func (r *TursoTaskRepository) DeleteTask(userID string, teamID string, id string) error {
return r.taskTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
// FirstUserOfTeam retourne le premier utilisateur d'une équipe. Les tâches
// planifiées ne portent pas d'utilisateur : c'est lui qui porte la conversation
// générée lors de l'exécution.
func (r *TursoTaskRepository) FirstUserOfTeam(teamID string) (string, error) {
var userID string
err := r.db.QueryRow(
"select user_id from user_teams where team_id=$1 order by _date_created asc limit 1",
teamID,
).Scan(&userID)
if err != nil {
return "", err
}
return userID, nil
}
@@ -0,0 +1,72 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoToolRepository struct {
db *sql.DB
ToolTable orm.Table[domain.Tool]
}
func NewTursoToolRepository(db *sql.DB) *TursoToolRepository {
dialect := orm.NewSqliteDialect()
ToolTable, err := orm.NewTable[domain.Tool](db, dialect, "tools")
if err != nil {
log.Fatalf("error creating lasebuche Tool table")
}
return &TursoToolRepository{
db: db,
ToolTable: ToolTable,
}
}
func (r *TursoToolRepository) ListTools(userID string, teamID string) ([]*domain.Tool, error) {
return r.ToolTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
)
}
func (r *TursoToolRepository) GetToolByName(userID string, teamID string, name string) (*domain.Tool, error) {
return r.ToolTable.SelectOne(
"name=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
name,
teamID,
userID,
)
}
func (r *TursoToolRepository) GetToolByID(userID string, teamID string, id string) (*domain.Tool, error) {
return r.ToolTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoToolRepository) CreateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
return r.ToolTable.Insert(Tool)
}
func (r *TursoToolRepository) UpdateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
return r.ToolTable.Update(Tool)
}
func (r *TursoToolRepository) DeleteTool(userID string, teamID string, name string) error {
return r.ToolTable.DeleteWhere(
"name=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
name,
teamID,
userID,
)
}
@@ -4,34 +4,33 @@ import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
"gitea.trankilou.fr/fabien/lasebuche"
)
type TursoUserRepository struct {
DB *sql.DB
UserTable lasebuche.Table[domain.User]
TeamTable lasebuche.Table[domain.Team]
UserTeamTable lasebuche.Table[domain.UserTeam]
AddressTable lasebuche.Table[domain.UserAddress]
UserTable orm.Table[domain.User]
TeamTable orm.Table[domain.Team]
UserTeamTable orm.Table[domain.UserTeam]
AddressTable orm.Table[domain.UserAddress]
}
func NewTursoUserRepository(db *sql.DB) *TursoUserRepository {
dialect := lasebuche.NewSqliteDialect()
userTable, err := lasebuche.NewTable[domain.User](db, dialect, "users")
dialect := orm.NewSqliteDialect()
userTable, err := orm.NewTable[domain.User](db, dialect, "users")
if err != nil {
log.Fatalf("error creating lasebuche user table")
}
teamTable, err := lasebuche.NewTable[domain.Team](db, dialect, "teams")
teamTable, err := orm.NewTable[domain.Team](db, dialect, "teams")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
userTeamTable, err := lasebuche.NewTable[domain.UserTeam](db, dialect, "user_teams")
userTeamTable, err := orm.NewTable[domain.UserTeam](db, dialect, "user_teams")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
userAddressTable, err := lasebuche.NewTable[domain.UserAddress](db, dialect, "user_addresses")
userAddressTable, err := orm.NewTable[domain.UserAddress](db, dialect, "user_addresses")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
@@ -53,7 +52,7 @@ func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, erro
}
func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) {
return ur.UserTable.SelectWhere("")
return ur.UserTable.Select()
}
func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) {
@@ -69,11 +68,14 @@ func (ur *TursoUserRepository) DeleteUser(id string) error {
}
func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.Team, error) {
return ur.TeamTable.SelectOne("id=$1 and user_id=$2", teamid, userid)
return ur.TeamTable.SelectOne("id=$1 and id in (select team_id from user_teams where user_id=$2)", teamid, userid)
}
func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) {
return ur.TeamTable.SelectWhere("id in (select team_id from user_teams where user_id=$1)", userid)
return ur.TeamTable.Select(
orm.WithWhere("id in (select team_id from user_teams where user_id=$1)", userid),
orm.WithOrder("label asc"),
)
}
func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) {
@@ -108,7 +110,9 @@ func (ur *TursoUserRepository) FindUserTeam(userid string, teamid string) (*doma
}
func (ur *TursoUserRepository) ListUserTeams(userid string) ([]*domain.UserTeam, error) {
return ur.UserTeamTable.SelectWhere("user_id=$1", userid)
return ur.UserTeamTable.Select(
orm.WithWhere("user_id=$1", userid),
)
}
func (ur *TursoUserRepository) CreateUserTeam(userid string, team *domain.UserTeam) (*domain.UserTeam, error) {
@@ -124,7 +128,10 @@ func (ur *TursoUserRepository) DeleteUserTeam(userid string, teamid string) erro
}
func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) {
return ur.AddressTable.SelectWhere("user_id=$1", id)
return ur.AddressTable.Select(
orm.WithWhere("user_id=$1", id),
orm.WithOrder("type asc"),
)
}
func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) {
+253
View File
@@ -0,0 +1,253 @@
package llm
import (
"context"
"fmt"
"log/slog"
anyllm "github.com/mozilla-ai/any-llm-go"
"github.com/mozilla-ai/any-llm-go/providers"
"github.com/mozilla-ai/any-llm-go/providers/anthropic"
"github.com/mozilla-ai/any-llm-go/providers/ollama"
"github.com/mozilla-ai/any-llm-go/providers/openai"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
var providerTypes = []domain.Item{
{Value: "anthropic", Label: "Anthropic"},
{Value: "openai", Label: "OpenAI"},
{Value: "ollama", Label: "Ollama"},
{Value: "openaicomp", Label: "OpenAI compatible"},
{Value: "openrouter", Label: "Openrouter"},
}
type AnyLLMEngine struct {
}
func NewAnyLLMEngine() *AnyLLMEngine {
return &AnyLLMEngine{}
}
func (e *AnyLLMEngine) ListProviderTypes() []domain.Item {
return providerTypes
}
func providerFactory(provider *domain.Provider) (anyllm.Provider, error) {
switch provider.Type {
case "ollama":
return ollama.New(
anyllm.WithBaseURL(provider.URL),
)
case "openai":
return openai.New(
anyllm.WithAPIKey(provider.APIKey),
)
case "openaicomp":
return openai.New(
anyllm.WithBaseURL(provider.URL),
anyllm.WithAPIKey(provider.APIKey),
)
case "anthropic":
return anthropic.New(
anyllm.WithAPIKey(provider.APIKey),
)
}
return nil, fmt.Errorf("unknown provider type: %s", provider.Type)
}
func (e *AnyLLMEngine) ListModelsFromProvider(ctx context.Context, provider *domain.Provider) ([]string, error) {
models := make([]string, 0)
prov, err := providerFactory(provider)
if err != nil {
return nil, err
}
if lister, ok := prov.(anyllm.ModelLister); ok {
response, err := lister.ListModels(ctx)
if err != nil {
return nil, err
}
for _, m := range response.Data {
models = append(models, m.ID)
}
} else {
models = append(models, "Default")
}
return models, nil
}
func (e *AnyLLMEngine) Stream(
ctx context.Context,
provider *domain.Provider,
modelID string,
params *domain.LLMParams,
messages []*domain.Message,
) *domain.Message {
p, err := providerFactory(provider)
if err != nil {
return &domain.Message{
Role: string(domain.RoleAssistant),
Content: err.Error(),
}
}
anyllmMessages := make([]anyllm.Message, 0)
for _, m := range messages {
anyllmMessages = append(anyllmMessages, anyllm.Message{
Role: m.Role,
Content: m.Content,
})
}
if params.ReasoningEffort == "" {
params.ReasoningEffort = "none"
}
tools := make([]providers.Tool, len(params.Tools))
for i, t := range params.Tools {
tools[i] = toolConvert(t)
}
toolCalling := false
toolCalls := make([]domain.ToolCall, 0)
chunkChan, errChan := p.CompletionStream(ctx, anyllm.CompletionParams{
Model: modelID,
Messages: anyllmMessages,
Stream: true,
ReasoningEffort: providers.ReasoningEffort(params.ReasoningEffort),
Tools: tools,
})
fullContent := ""
for chunk := range chunkChan {
if len(chunk.Choices) > 0 {
content := chunk.Choices[0].Delta.Content
reasoning := chunk.Choices[0].Delta.Reasoning
if params.OnChunk != nil {
if content != "" {
fullContent += content
params.OnChunk(&domain.Chunk{
Done: false,
Role: anyllm.RoleAssistant,
Content: content,
Reasoning: false,
})
} else if reasoning != nil {
params.OnChunk(&domain.Chunk{
Done: false,
Role: anyllm.RoleAssistant,
Content: reasoning.Content,
Reasoning: true,
})
}
}
for _, tc := range chunk.Choices[0].Delta.ToolCalls {
toolCalling = true
slog.Info("Model is calling tool", "tool", tc.Function.Name, "arguments", tc.Function.Arguments)
extra := make(map[string]map[string]any)
for k, v := range tc.Extra {
m := map[string]any(v)
extra[k] = m
}
toolCalls = append(toolCalls, domain.ToolCall{
ID: tc.ID,
Type: tc.Type,
Function: domain.ToolCallFunction{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
Extra: extra,
})
if params.OnChunk != nil {
params.OnChunk(&domain.Chunk{
Done: false,
ToolName: tc.Function.Name,
Role: anyllm.RoleAssistant,
Content: tc.Function.Arguments,
Reasoning: false,
})
}
}
}
}
if !toolCalling {
if params.OnChunk != nil {
params.OnChunk(&domain.Chunk{
Done: true,
Role: anyllm.RoleAssistant,
Content: "",
})
}
}
if err := <-errChan; err != nil {
return &domain.Message{
Role: string(domain.RoleAssistant),
Content: err.Error(),
}
}
return &domain.Message{
Role: string(domain.RoleAssistant),
Content: fullContent,
ToolCalls: toolCalls,
}
}
func (e *AnyLLMEngine) Generate(
ctx context.Context,
provider *domain.Provider,
modelID string,
messages []*domain.Message,
) (string, error) {
p, err := providerFactory(provider)
if err != nil {
return "", err
}
anyllmMessages := make([]anyllm.Message, 0)
for _, m := range messages {
anyllmMessages = append(anyllmMessages, anyllm.Message{
Role: m.Role,
Content: m.Content,
})
}
response, err := p.Completion(ctx, anyllm.CompletionParams{
Model: modelID,
Messages: anyllmMessages,
Stream: false,
})
if err != nil {
return "", err
}
return response.Choices[0].Message.Content.(string), nil
}
func toolConvert(domainTool *domain.ToolDefinition) providers.Tool {
return providers.Tool{
Type: domainTool.Type,
Function: providers.Function{
Name: domainTool.Function.Name,
Description: domainTool.Function.Description,
Parameters: domainTool.Function.Parameters,
},
}
}
-76
View File
@@ -1,76 +0,0 @@
package llm
import (
"context"
"fmt"
anyllm "github.com/mozilla-ai/any-llm-go"
"github.com/mozilla-ai/any-llm-go/providers/anthropic"
"github.com/mozilla-ai/any-llm-go/providers/ollama"
"github.com/mozilla-ai/any-llm-go/providers/openai"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
var providerTypes = []domain.Item{
{ID: "anthropic", Text: "Anthropic"},
{ID: "openai", Text: "OpenAI"},
{ID: "ollama", Text: "Ollama"},
{ID: "openaicomp", Text: "OpenAI compatible"},
{ID: "openrouter", Text: "Openrouter"},
}
type AnyLLMEngine struct {
}
func NewAnyLLMEngine() *AnyLLMEngine {
return &AnyLLMEngine{}
}
func (e *AnyLLMEngine) ListProviderTypes() []domain.Item {
return providerTypes
}
func providerFactory(provider *domain.Provider) (anyllm.Provider, error) {
switch provider.Type {
case "ollama":
return ollama.New(
anyllm.WithBaseURL(provider.URL),
)
case "openai":
return openai.New(
anyllm.WithAPIKey(provider.APIKey),
)
case "openaicomp":
return openai.New(
anyllm.WithBaseURL(provider.URL),
anyllm.WithAPIKey(provider.APIKey),
)
case "anthropic":
return anthropic.New(
anyllm.WithAPIKey(provider.APIKey),
)
}
return nil, fmt.Errorf("unknown provider type: %s", provider.Type)
}
func (e *AnyLLMEngine) ListModelsFromProvider(ctx context.Context, provider *domain.Provider) ([]string, error) {
models := make([]string, 0)
prov, err := providerFactory(provider)
if err != nil {
return nil, err
}
if lister, ok := prov.(anyllm.ModelLister); ok {
response, err := lister.ListModels(ctx)
if err != nil {
return nil, err
}
for _, m := range response.Data {
models = append(models, m.ID)
}
} else {
models = append(models, "Default")
}
return models, nil
}
+32
View File
@@ -75,6 +75,38 @@ func (tm *JwtTokenManager) GenerateRefreshToken(userID string) (string, time.Tim
return signed, expiresAt, err
}
// GenerateStateToken crée un JWT court (10 min) utilisé comme paramètre state
// du flux OIDC, porteur d'une donnée arbitraire (ex: l'id du fournisseur).
func (tm *JwtTokenManager) GenerateStateToken(data string) (string, error) {
claims := jwt.RegisteredClaims{
Subject: data,
Issuer: tm.issuer,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(tm.secret)
}
// ParseStateToken vérifie un state généré par GenerateStateToken et retourne
// la donnée qu'il transporte.
func (tm *JwtTokenManager) ParseStateToken(state string) (string, error) {
claims := &jwt.RegisteredClaims{}
token, err := jwt.ParseWithClaims(state, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("méthode de signature inattendue")
}
return tm.secret, nil
})
if err != nil {
return "", err
}
if !token.Valid {
return "", errors.New("token invalide")
}
return claims.Subject, nil
}
// ParseAndValidate décode et vérifie la signature + l'expiration d'un JWT.
func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, error) {
claims := &Claims{}
@@ -0,0 +1,314 @@
package browser
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/stealth"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type BrowserConfig struct {
// Headless est un pointeur pour distinguer "non renseigné" (défaut : true) de "explicitement false".
Headless *bool `yaml:"headless"`
TimeoutSeconds int `yaml:"timeoutSeconds"`
}
// BrowserTool maintains a persistent browser session across LLM tool calls.
type BrowserTool struct{}
var (
browserInstance *rod.Browser
currentPage *rod.Page
browserMu sync.Mutex
launchedHeadless bool
)
func (t *BrowserTool) Name() string {
return "browser"
}
func (t *BrowserTool) Description() string {
return "Control a web browser to navigate pages and interact with content. " +
"Maintains session state across calls (cookies, current page). " +
"Actions: 'navigate' (go to URL), 'click' (click element by CSS selector), " +
"'fill' (type into an input field), 'content' (get current page text), " +
"'links' (list all links on current page)."
}
func (t *BrowserTool) Params() []domain.ToolParam {
return []domain.ToolParam{
{JsonName: "headless", Type: "bool", Mandatory: true, Description: "Headless"},
{JsonName: "timeoutSeconds", Type: "int", Mandatory: true, Description: "Timeout en secondes"},
}
}
func (t *BrowserTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"navigate", "click", "fill", "content", "links"},
"description": "The browser action to perform.",
},
"url": map[string]any{
"type": "string",
"description": "URL to navigate to. Required for action='navigate'.",
},
"selector": map[string]any{
"type": "string",
"description": "CSS selector of the element to interact with. Required for 'click' and 'fill'.",
},
"value": map[string]any{
"type": "string",
"description": "Text value to type into the field. Required for action='fill'.",
},
},
"required": []string{"action"},
},
},
}
}
func (t *BrowserTool) Execute(_ context.Context, input []byte, configuration string) ([]byte, error) {
slog.Debug("[browser]", "input", string(input))
var params struct {
Action string `json:"action"`
URL string `json:"url"`
Selector string `json:"selector"`
Value string `json:"value"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
var toolConfig BrowserConfig
err := json.Unmarshal([]byte(configuration), &toolConfig)
if err != nil {
return nil, err
}
timeout := time.Duration(toolConfig.TimeoutSeconds) * time.Second
// Headless est nil quand la configuration ne le précise pas : défaut à true.
headless := true
if toolConfig.Headless != nil {
headless = *toolConfig.Headless
}
if timeout <= 0 {
timeout = 60 * time.Second
}
browserMu.Lock()
defer browserMu.Unlock()
if err := ensureBrowser(headless); err != nil {
return nil, fmt.Errorf("browser init failed: %w", err)
}
switch params.Action {
case "navigate":
if params.URL == "" {
return nil, fmt.Errorf("'url' is required for action 'navigate'")
}
return t.navigate(params.URL, timeout)
case "click":
if params.Selector == "" {
return nil, fmt.Errorf("'selector' is required for action 'click'")
}
return t.click(params.Selector, timeout)
case "fill":
if params.Selector == "" || params.Value == "" {
return nil, fmt.Errorf("'selector' and 'value' are required for action 'fill'")
}
return t.fill(params.Selector, params.Value, timeout)
case "content":
return t.pageContent(timeout)
case "links":
return t.pageLinks(timeout)
default:
return nil, fmt.Errorf("unknown action %q", params.Action)
}
}
func browserProfileDir() string {
if dir, err := os.UserCacheDir(); err == nil {
return filepath.Join(dir, "lassistanoque", "browser-profile")
}
return filepath.Join(os.TempDir(), "lassistanoque-browser-profile")
}
// ensureBrowser lance le navigateur au premier appel et le relance si le mode
// headless a changé dans la configuration.
func ensureBrowser(headless bool) error {
if browserInstance != nil {
if headless == launchedHeadless {
return nil
}
_ = browserInstance.Close()
browserInstance = nil
currentPage = nil
}
if err := initBrowser(headless); err != nil {
return err
}
launchedHeadless = headless
return nil
}
func initBrowser(headless bool) error {
path, _ := launcher.LookPath()
controlURL, err := launcher.New().
Bin(path).
Headless(headless).
UserDataDir(browserProfileDir()).
Delete("enable-automation").
Launch()
if err != nil {
return fmt.Errorf("failed to launch browser: %w", err)
}
b := rod.New().ControlURL(controlURL)
if err := b.Connect(); err != nil {
return fmt.Errorf("failed to connect to browser: %w", err)
}
browserInstance = b
return nil
}
func ensurePage(timeout time.Duration) error {
if currentPage == nil {
page, err := stealth.Page(browserInstance.Timeout(timeout))
if err != nil {
return fmt.Errorf("failed to open page: %w", err)
}
currentPage = page
}
return nil
}
func (t *BrowserTool) navigate(rawURL string, timeout time.Duration) ([]byte, error) {
if err := ensurePage(timeout); err != nil {
return nil, err
}
if err := currentPage.Timeout(timeout).Navigate(rawURL); err != nil {
return nil, fmt.Errorf("navigation failed: %w", err)
}
if err := currentPage.Timeout(timeout).WaitLoad(); err != nil {
return nil, fmt.Errorf("page load failed: %w", err)
}
return t.pageContent(timeout)
}
func (t *BrowserTool) click(selector string, timeout time.Duration) ([]byte, error) {
if err := ensurePage(timeout); err != nil {
return nil, err
}
el, err := currentPage.Timeout(timeout).Element(selector)
if err != nil {
return nil, fmt.Errorf("element %q not found: %w", selector, err)
}
if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil {
return nil, fmt.Errorf("click failed: %w", err)
}
if err := currentPage.WaitIdle(timeout); err != nil {
return nil, fmt.Errorf("wait after click failed: %w", err)
}
return t.pageContent(timeout)
}
func (t *BrowserTool) fill(selector, value string, timeout time.Duration) ([]byte, error) {
if err := ensurePage(timeout); err != nil {
return nil, err
}
el, err := currentPage.Timeout(timeout).Element(selector)
if err != nil {
return nil, fmt.Errorf("element %q not found: %w", selector, err)
}
if err := el.SelectAllText(); err != nil {
return nil, fmt.Errorf("failed to select text: %w", err)
}
if err := el.Input(value); err != nil {
return nil, fmt.Errorf("fill failed: %w", err)
}
return t.pageContent(timeout)
}
func (t *BrowserTool) pageContent(timeout time.Duration) ([]byte, error) {
if currentPage == nil {
return []byte("No page loaded."), nil
}
info, err := currentPage.Info()
if err != nil {
return nil, fmt.Errorf("failed to get page info: %w", err)
}
titleRes, err := currentPage.Eval("() => document.title")
if err != nil {
return nil, fmt.Errorf("failed to get page title: %w", err)
}
title := titleRes.Value.String()
body, err := currentPage.Timeout(timeout).Element("body")
if err != nil {
return nil, fmt.Errorf("failed to get page body: %w", err)
}
text, err := body.Text()
if err != nil {
return nil, fmt.Errorf("failed to extract page text: %w", err)
}
var sb strings.Builder
fmt.Fprintf(&sb, "Page: %s\nURL: %s\n\n%s", title, info.URL, text)
return []byte(sb.String()), nil
}
func (t *BrowserTool) pageLinks(timeout time.Duration) ([]byte, error) {
if currentPage == nil {
return []byte("No page loaded."), nil
}
elements, err := currentPage.Timeout(timeout).Elements("a[href]")
if err != nil {
return nil, fmt.Errorf("failed to get links: %w", err)
}
var sb strings.Builder
fmt.Fprintf(&sb, "Links on current page (%d):\n\n", len(elements))
for i, el := range elements {
href, _ := el.Attribute("href")
text, _ := el.Text()
if href == nil {
continue
}
label := strings.TrimSpace(text)
if label == "" {
label = "(no text)"
}
fmt.Fprintf(&sb, "%d. %s — %s\n", i+1, label, *href)
}
return []byte(sb.String()), nil
}
@@ -0,0 +1,186 @@
package scheduler
import (
"context"
"encoding/json"
"fmt"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/task"
)
// SchedulerTool permet à l'agent de planifier des tâches pour l'utilisateur :
// exécution d'un prompt selon une spécification cron, avec ou sans répétition.
type SchedulerTool struct {
taskService *task.Service
}
func New(taskService *task.Service) *SchedulerTool {
return &SchedulerTool{taskService: taskService}
}
func (t *SchedulerTool) Name() string {
return "scheduler"
}
func (t *SchedulerTool) Description() string {
return "Schedule tasks that run automatically at specified times, with or without repetition. " +
"Use this tool whenever the user asks to do something later or on a recurring basis " +
"(e.g. 'every day at 8am', 'every Monday at 9am', 'tomorrow at 6pm', 'every hour'). " +
"Actions: 'create' (schedule a new task), 'list' (list scheduled tasks), " +
"'update' (modify a task's title, prompt, cron or repetition), " +
"'delete' (delete a task by its id)."
}
func (t *SchedulerTool) Params() []domain.ToolParam {
return []domain.ToolParam{}
}
func (t *SchedulerTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"create", "list", "update", "delete"},
"description": "The scheduling action to perform.",
},
"title": map[string]any{
"type": "string",
"description": "Short task title, e.g. 'Météo du matin'. Required for 'create'.",
},
"prompt": map[string]any{
"type": "string",
"description": "Self-contained prompt to execute at the scheduled time. " +
"It will run in a fresh conversation with no memory of this one: " +
"include all needed context. Required for 'create'.",
},
"cron": map[string]any{
"type": "string",
"description": "Cron spec, 5 fields 'minute hour day-of-month month day-of-week' " +
"(e.g. '0 8 * * *' every day at 08:00, '0 9 * * 1' every Monday at 09:00, " +
"'30 18 1 * *' on the 1st at 18:30) or a descriptor like '@daily', '@hourly', '@weekly'. " +
"Required for 'create'.",
},
"repeat": map[string]any{
"type": "boolean",
"description": "true: the task repeats on schedule. " +
"false: it runs only once, at the first time matching the cron spec, then is marked done. " +
"Default true. Use false for one-shot requests like 'tomorrow at 8am'.",
},
"id": map[string]any{
"type": "string",
"description": "Task id, as returned by 'list'. Required for 'update' and 'delete'.",
},
},
"required": []string{"action"},
},
},
}
}
func (t *SchedulerTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
var params struct {
Action string `json:"action"`
Title string `json:"title"`
Prompt string `json:"prompt"`
Cron string `json:"cron"`
Repeat *bool `json:"repeat"`
ID string `json:"id"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
userID, ok := domain.UserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("scheduler tool: missing user context")
}
teamID, ok := domain.TeamIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("scheduler tool: missing team context")
}
modelID, _ := domain.ModelIDFromContext(ctx)
switch params.Action {
case "create":
repeat := true
if params.Repeat != nil {
repeat = *params.Repeat
}
taskModel, err := t.taskService.CreateTask(userID, teamID, &domain.Task{
ModelID: modelID,
Label: params.Title,
Prompt: params.Prompt,
Cron: params.Cron,
Repeat: repeat,
})
if err != nil {
return nil, err
}
result, err := json.Marshal(taskModel)
if err != nil {
return nil, err
}
return result, nil
case "list":
tasks, err := t.taskService.ListTasks(userID, teamID)
if err != nil {
return nil, err
}
result, err := json.Marshal(tasks)
if err != nil {
return nil, err
}
return result, nil
case "update":
if params.ID == "" {
return nil, fmt.Errorf("'id' is required for action 'update'")
}
current, err := t.taskService.GetTask(userID, teamID, params.ID)
if err != nil {
return nil, err
}
// mise à jour partielle : seuls les champs fournis changent
if params.Title != "" {
current.Label = params.Title
}
if params.Prompt != "" {
current.Prompt = params.Prompt
}
if params.Cron != "" {
current.Cron = params.Cron
}
if params.Repeat != nil {
current.Repeat = *params.Repeat
}
updated, err := t.taskService.UpdateTask(userID, teamID, current)
if err != nil {
return nil, err
}
result, err := json.Marshal(updated)
if err != nil {
return nil, err
}
return result, nil
case "delete":
if params.ID == "" {
return nil, fmt.Errorf("'id' is required for action 'delete'")
}
if err := t.taskService.DeleteTask(userID, teamID, params.ID); err != nil {
return nil, err
}
return []byte("task deleted"), nil
default:
return nil, fmt.Errorf("unknown action %q", params.Action)
}
}
@@ -0,0 +1,151 @@
package searxng
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type SearxngConfig struct {
URL string `json:"url"`
}
type SearxngTool struct {
}
func (t *SearxngTool) Name() string {
return "searxng"
}
func (t *SearxngTool) Description() string {
return "Search the web using a SearXNG instance. Returns a list of relevant results with title, URL, and snippet."
}
func (t *SearxngTool) Params() []domain.ToolParam {
return []domain.ToolParam{
{JsonName: "url", Type: "string", Mandatory: true, Description: "Searxng URL"},
}
}
func (t *SearxngTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "The search query",
},
"categories": map[string]any{
"type": "string",
"description": "Comma-separated list of categories: general, news, images, science, it, social_media. Defaults to 'general'.",
},
"language": map[string]any{
"type": "string",
"description": "Language code for results, e.g. 'fr', 'en'. Defaults to 'all'.",
},
},
"required": []string{"query"},
},
},
}
}
type searxngResponse struct {
Results []searxngResult `json:"results"`
}
type searxngResult struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
Engine string `json:"engine"`
}
func (t *SearxngTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
slog.Debug("[searxng]", "input", string(input))
var params struct {
Query string `json:"query"`
Categories string `json:"categories"`
Language string `json:"language"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
var toolCfg SearxngConfig
err := json.Unmarshal([]byte(configuration), &toolCfg)
if err != nil {
return nil, fmt.Errorf("searxng tool not configured: %w", err)
}
if toolCfg.URL == "" {
return nil, fmt.Errorf("searxng tool config missing 'url' field")
}
baseURL := toolCfg.URL
categories := "general"
if params.Categories != "" {
categories = params.Categories
}
language := "all"
if params.Language != "" {
language = params.Language
}
reqURL := fmt.Sprintf("%s/search?q=%s&format=json&categories=%s&language=%s",
strings.TrimRight(baseURL, "/"),
url.QueryEscape(params.Query),
url.QueryEscape(categories),
url.QueryEscape(language),
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("searxng request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("searxng returned status %d", resp.StatusCode)
}
var searchResp searxngResponse
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(searchResp.Results) == 0 {
return []byte("No results found."), nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "Search results for \"%s\":\n\n", params.Query)
for i, r := range searchResp.Results {
fmt.Fprintf(&sb, "%d. %s\n", i+1, r.Title)
fmt.Fprintf(&sb, " URL: %s\n", r.URL)
if r.Content != "" {
fmt.Fprintf(&sb, " %s\n", r.Content)
}
fmt.Fprintf(&sb, "\n")
}
return []byte(sb.String()), nil
}
@@ -0,0 +1,187 @@
package weather
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type WeatherTool struct {
}
type WeatherConfig struct {
}
func (t *WeatherTool) Name() string {
return "weather"
}
func (t *WeatherTool) Description() string {
return "Get the current weather for a given location"
}
func (t *WeatherTool) Params() []domain.ToolParam {
return []domain.ToolParam{}
}
func (t *WeatherTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{
"type": "string",
"description": "The city name, e.g., 'Paris' or 'New York'",
},
},
"required": []string{"location"},
},
},
}
}
func (t *WeatherTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
var params struct {
Location string `json:"location"`
}
err := json.Unmarshal(input, &params)
if err != nil {
return nil, err
}
resp, err := http.Get("https://geocoding-api.open-meteo.com/v1/search?name=" + url.QueryEscape(params.Location))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var location struct {
Results []struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"results"`
}
err = json.NewDecoder(resp.Body).Decode(&location)
if err != nil {
return nil, err
}
if len(location.Results) == 0 {
return nil, fmt.Errorf("location not found : %s", input)
}
lon := location.Results[0].Longitude
lat := location.Results[0].Latitude
weatherResp, err := http.Get(fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&current_weather=true&daily=weather_code,temperature_2m_max,temperature_2m_min,rain_sum,wind_speed_10m_max&hourly=temperature_2m&forecast_days=15", lat, lon))
if err != nil {
return nil, err
}
defer weatherResp.Body.Close()
var weatherData struct {
CurrentWeatherUnits struct {
Temperature string `json:"temperature"`
WindSpeed string `json:"windspeed"`
} `json:"current_weather_units"`
CurrentWeather struct {
Time string `json:"time"`
Temperature float64 `json:"temperature"`
WeatherCode int `json:"weathercode"`
} `json:"current_weather"`
DailyUnits struct {
TemperatureMax string `json:"temperature_2m_max"`
TemperatureMin string `json:"temperature_2m_min"`
Rain string `json:"rain_sum"`
WindSpeed string `json:"wind_speed_10m_max"`
} `json:"daily_units"`
Daily struct {
Time []string `json:"time"`
TemperatureMax []float64 `json:"temperature_2m_max"`
TemperatureMin []float64 `json:"temperature_2m_min"`
Rain []float64 `json:"rain_sum"`
WindSpeed []float64 `json:"wind_speed_10m_max"`
} `json:"daily"`
}
err = json.NewDecoder(weatherResp.Body).Decode(&weatherData)
if err != nil {
return nil, err
}
var builder strings.Builder
fmt.Fprintf(&builder, "Current weather in %s\n", params.Location)
fmt.Fprintf(&builder, "- Time: %s\n", weatherData.CurrentWeather.Time)
fmt.Fprintf(&builder, "- Temperature: %.2f %s\n", weatherData.CurrentWeather.Temperature, weatherData.CurrentWeatherUnits.Temperature)
fmt.Fprintf(&builder, "- Weather code: %d - %s\n", weatherData.CurrentWeather.WeatherCode, wmoCodeToDescription(weatherData.CurrentWeather.WeatherCode))
if len(weatherData.Daily.WindSpeed) > 0 {
fmt.Fprintf(&builder, "- Wind speed: %.2f %s\n", weatherData.Daily.WindSpeed[0], weatherData.DailyUnits.WindSpeed)
}
if len(weatherData.Daily.TemperatureMax) > 0 {
fmt.Fprintf(&builder, "- Max temperature: %.2f %s\n", weatherData.Daily.TemperatureMax[0], weatherData.DailyUnits.TemperatureMax)
}
if len(weatherData.Daily.TemperatureMin) > 0 {
fmt.Fprintf(&builder, "- Min temperature: %.2f %s\n", weatherData.Daily.TemperatureMin[0], weatherData.DailyUnits.TemperatureMin)
}
if len(weatherData.Daily.Rain) > 0 {
fmt.Fprintf(&builder, "- Rainfall: %.2f %s\n", weatherData.Daily.Rain[0], weatherData.DailyUnits.Rain)
}
fmt.Fprintf(&builder, "\nForecast for the next days:\n")
// Iterate over the daily data and append to the repor
for i := 0; i < len(weatherData.Daily.Time); i++ {
fmt.Fprintf(&builder, "%s: Max Temp: %.2f %s, Min Temp: %.2f %s, Rain: %.2f %s, Wind Speed: %.2f %s\n",
weatherData.Daily.Time[i],
weatherData.Daily.TemperatureMax[i], weatherData.DailyUnits.TemperatureMax,
weatherData.Daily.TemperatureMin[i], weatherData.DailyUnits.TemperatureMin,
weatherData.Daily.Rain[i], weatherData.DailyUnits.Rain,
weatherData.Daily.WindSpeed[i], weatherData.DailyUnits.WindSpeed)
}
return []byte(builder.String()), nil
}
func wmoCodeToDescription(code int) string {
switch code {
case 0:
return "Clear sky"
case 1, 2, 3:
return "Mainly clear, partly cloudy, and overcast"
case 45, 48:
return "Fog and depositing rime fog"
case 51, 53, 55:
return "Drizzle: Light, moderate, and dense intensity"
case 56, 57:
return "Freezing Drizzle: Light and dense intensity"
case 61, 63, 65:
return "Rain: Slight, moderate and heavy intensity"
case 66, 67:
return "Freezing Rain: Light and heavy intensity"
case 71, 73, 75:
return "Snowfall: Slight, moderate, and heavy intensity"
case 77:
return "Snow grains"
case 80, 81, 82:
return "Rain showers: Slight, moderate, and violent"
case 85, 86:
return "Snow showers slight and heavy"
case 95:
return "Thunderstorm: Slight and heavy intensity"
case 96, 99:
return "Thunderstorm with slight and heavy hail"
default:
return "N/A"
}
}
+8
View File
@@ -13,6 +13,7 @@ const (
DEFAULT_DB_URL = "lassistanoque.db"
DEFAULT_HTTP_PORT = 3000
DEFAULT_STORAGE_TYPE = "database"
DEFAULT_SERVER_ID = "default"
)
type Config struct {
@@ -27,6 +28,8 @@ type Config struct {
StorageS3AccessKeyID string // S3
StorageS3AccessKeySecret string // S3
StorageS3UseSSL string // S3
ServerID string
SearXngUrl string
}
var config *Config
@@ -56,6 +59,10 @@ func GetConfig() *Config {
if storageType == "" {
storageType = DEFAULT_STORAGE_TYPE
}
serverID := os.Getenv("LASSISTANOQUE_SERVER_ID")
if serverID == "" {
serverID = DEFAULT_SERVER_ID
}
config = &Config{
DatabaseType: databaseType,
@@ -63,6 +70,7 @@ func GetConfig() *Config {
HttpPort: httpPort,
JWTSecret: jwtsecret,
StorageType: storageType,
ServerID: serverID,
}
}
+28
View File
@@ -0,0 +1,28 @@
package domain
import "time"
type Agent struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
Name string `db:"name" json:"name"`
SystemPrompt string `db:"system_prompt" json:"systemPrompt"`
DefaultProviderID string `db:"def_provider_id" json:"defaultProviderId"`
DefaultModelID string `db:"def_model_id" json:"defaultModelId"`
ToolsPolicy string `db:"tools_policy" json:"toolsPolicy"`
SubAgents bool `db:"sub_agents" json:"subAgents"`
LoopStrategy string `db:"loop_strategy" json:"loopStrategy"`
MaxIterations string `db:"max_iterations" json:"maxIterations"`
StoppingCriteria string `db:"stopping_criteria" json:"stoppingCriteria"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type AgentRepository interface {
ListAgents(userID string, teamID string) ([]*Agent, error)
GetAgent(userID string, teamID string, id string) (*Agent, error)
CreateAgent(userID string, agent *Agent) (*Agent, error)
UpdateAgent(userID string, agent *Agent) (*Agent, error)
DeleteAgent(userID string, teamID string, id string) error
}
+151 -15
View File
@@ -1,26 +1,162 @@
package domain
import "context"
import (
"context"
"encoding/json"
"time"
)
type LLMModelDetail struct {
ParentModel string `json:"parent_model"`
Format string `json:"format"`
Family string `json:"family"`
Families []string `json:"families"`
ParameterSize string `json:"parameter_size"`
QuantizationLevel string `json:"quantization_level"`
type Chat struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
StartDatetime *time.Time `db:"start_datetime" json:"startDatetime"`
EndDatetime *time.Time `db:"end_datetime" json:"endDatetime"`
AgentID string `db:"agent_id" json:"agentId"`
UserID string `db:"user_id" json:"userId"`
TaskID string `db:"task_id" json:"taskId,omitempty"`
Title string `db:"title" json:"title"`
FreshTitle bool `db:"fresh_title" json:"freshTitle"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type LLMModel struct {
Name string `json:"name"`
Model string `json:"model"`
ModifiedAt int64 `json:"modified_at"`
Size int64 `json:"size"`
Digest string `json:"digest"`
Detail *LLMModelDetail `json:"detail"`
type Message struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
ChatID string `db:"chat_id" json:"chatId"`
Role string `db:"role" json:"role"`
Content string `db:"content" json:"content"`
ToolCallID string `db:"tool_call_id" json:"toolCallId"`
ToolCalls []ToolCall `db:"-" json:"toolcalls"`
ToolCallsJson string `db:"tool_calls" json:"-"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
func (msg *Message) ToolCall2Json() error {
if msg.ToolCalls != nil {
toolCallJson, err := json.Marshal(&msg.ToolCalls)
if err != nil {
return err
}
msg.ToolCallsJson = string(toolCallJson)
}
return nil
}
func (msg *Message) Json2toolCall() error {
if msg.ToolCallsJson != "" {
err := json.Unmarshal([]byte(msg.ToolCallsJson), &msg.ToolCalls)
if err != nil {
return err
}
}
return nil
}
type ChatWithMessages struct {
Chat *Chat `json:"chat"`
Messages []*Message `json:"messages"`
}
type ChatParams struct {
ProviderID string
ModelID string
ContextMaxTokens int // taille de contexte du modèle ; 0 = défaut
MaxResponseTokens int // réserve de réponse ; 0 = défaut
OnChunk StreamCallback
}
type ToolParam struct {
JsonName string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Mandatory bool `json:"mandatory"`
}
type ITool interface {
Name() string
Description() string
Params() []ToolParam
Definition(ctx context.Context) *ToolDefinition
Execute(ctx context.Context, input []byte, configuration string) ([]byte, error)
}
type ToolDefinition struct {
Type string
Function *ToolFunction
}
type ToolFunction struct {
Name string
Description string
Parameters map[string]any
}
type ToolCall struct {
ID string
Type string
Function ToolCallFunction
Extra map[string]map[string]any
}
type ToolCallFunction struct {
Name string
Arguments string
}
type Chunk struct {
Done bool `json:"done"`
ToolName string `json:"toolName,omitempty"`
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
Reasoning bool `json:"reasoning,omitempty"`
}
type StreamCallback func(chunk *Chunk)
type Role string
const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
RoleTool Role = "tool"
)
type LLMParams struct {
Tools []*ToolDefinition
OnChunk StreamCallback
ReasoningEffort string
}
type LLMEngine interface {
ListProviderTypes() []Item
ListModelsFromProvider(ctx context.Context, provider *Provider) ([]string, error)
Stream(
ctx context.Context,
provider *Provider,
modelID string,
params *LLMParams,
messages []*Message,
) *Message
Generate(
ctx context.Context,
provider *Provider,
modelID string,
messages []*Message,
) (string, error)
}
type ChatRepository interface {
ListChats(userID string, teamID string, page int, query string, taskID string) ([]*Chat, error)
ListChatsWithUnfreshTitles(teamID string) ([]*Chat, error)
GetChat(userID string, teamID string, id string) (*Chat, error)
CreateChat(userID string, chat *Chat) (*Chat, error)
UpdateChat(userID string, chat *Chat) (*Chat, error)
DeleteChat(userID string, teamID string, id string) error
GetChatMessages(userID string, teamID string, id string, withToolCallResponses bool) ([]*Message, error)
CreateChatMessage(userID string, message *Message) (*Message, error)
}
+21
View File
@@ -0,0 +1,21 @@
package domain
import "time"
// OIDCProvider décrit un fournisseur OpenID Connect enregistré dans la table oidc.
type OIDCProvider struct {
ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"`
Domain string `db:"domain" json:"domain"`
ClientID string `db:"client_id" json:"clientId"`
ClientSecret string `db:"client_secret" json:"-"`
WellknownURL string `db:"wellknown_url" json:"wellknownUrl"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type OIDCProviderRepository interface {
ListOIDCProviders() ([]*OIDCProvider, error)
FindOIDCProvider(id string) (*OIDCProvider, error)
}
+8 -2
View File
@@ -1,6 +1,12 @@
package domain
type Item struct {
ID string `json:"id"`
Text string `json:"text"`
Value string `json:"value"`
Label string `json:"label"`
}
type ComplexItem struct {
Value string `json:"value"`
Label string `json:"label"`
Group string `json:"group"`
}
+35
View File
@@ -0,0 +1,35 @@
package domain
import "time"
const (
// statuts d'une tâche planifiée
TaskStatusActive = "active"
TaskStatusDone = "done"
)
type Task struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
ModelID string `db:"model_id" json:"modelId"`
Label string `db:"label" json:"label"`
Prompt string `db:"prompt" json:"prompt"`
Cron string `db:"cron" json:"cron"`
Repeat bool `db:"repeat" json:"repeat"`
Status string `db:"status" json:"status"`
NextDatetime *time.Time `db:"next_datetime" json:"nextDatetime"`
LastDatetime *time.Time `db:"last_datetime" json:"lastDatetime"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type TaskRepository interface {
ListTasks(userID string, teamID string) ([]*Task, error)
ListActiveTasks() ([]*Task, error)
GetTaskByID(userID string, teamID string, id string) (*Task, error)
CreateTask(task *Task) (*Task, error)
UpdateTask(task *Task) (*Task, error)
DeleteTask(userID string, teamID string, id string) error
FirstUserOfTeam(teamID string) (string, error)
}
+58
View File
@@ -0,0 +1,58 @@
package domain
import (
"context"
"time"
)
// Clés de contexte passées aux outils lors de l'exécution : identité de
// l'appelant et modèle en cours, pour les outils qui en dépendent
// (ex. scheduler crée une tâche pour l'équipe courante).
type contextKey int
const (
contextKeyUserID contextKey = iota
contextKeyTeamID
contextKeyModelID
)
func WithToolContext(ctx context.Context, userID string, teamID string, modelID string) context.Context {
ctx = context.WithValue(ctx, contextKeyUserID, userID)
ctx = context.WithValue(ctx, contextKeyTeamID, teamID)
return context.WithValue(ctx, contextKeyModelID, modelID)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(contextKeyUserID).(string)
return v, ok
}
func TeamIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(contextKeyTeamID).(string)
return v, ok
}
func ModelIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(contextKeyModelID).(string)
return v, ok
}
type Tool struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
Name string `db:"name" json:"name"`
Enabled bool `db:"enabled" json:"enabled"`
Configuration string `db:"configuration" json:"configuration"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type ToolRepository interface {
ListTools(userID string, teamID string) ([]*Tool, error)
GetToolByName(userID string, teamID string, name string) (*Tool, error)
GetToolByID(userID string, teamID string, id string) (*Tool, error)
CreateTool(userID string, agent *Tool) (*Tool, error)
UpdateTool(userID string, agent *Tool) (*Tool, error)
DeleteTool(userID string, teamID string, name string) error
}
+1 -1
View File
@@ -23,7 +23,7 @@ type User struct {
type Team struct {
ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"`
DefaultModelID *string `db:"default_model_id" json:"defaultModelId"`
DefaultAgentID *string `db:"default_agent_id" json:"defaultAgentId"`
DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"`
+66
View File
@@ -0,0 +1,66 @@
package gateway
import (
"context"
"fmt"
"sync"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
)
type Dependencies struct {
ChatRepository domain.ChatRepository
}
type Router struct {
started bool
mu sync.Mutex
sessions map[string]*Session // running sessions
newSessionChan chan string
closeChan chan bool
}
func NewRouter(deps Dependencies) *Router {
return &Router{
started: true,
sessions: make(map[string]*Session),
newSessionChan: make(chan string),
closeChan: make(chan bool),
}
}
func (s *Router) Start() error {
for {
select {
case _ = <-s.closeChan:
fmt.Println("Closing LLM Server")
return nil
}
}
}
func (s *Router) Stop(ctx context.Context) error {
s.closeChan <- true
s.started = false
return nil
}
func (s *Router) StartSession() (*Session, error) {
sid := utility.GenID()
session := &Session{}
s.mu.Lock()
s.sessions[sid] = session
s.mu.Unlock()
return session, nil
}
func (s *Router) Process(ctx context.Context, Chat *domain.ChatWithMessages) error {
s.closeChan <- true
s.started = false
return nil
}
+6
View File
@@ -0,0 +1,6 @@
package gateway
type Session struct {
serverID string
chatID string
}
+117
View File
@@ -0,0 +1,117 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/agent"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewAgentGroup(
prefix string,
e *echo.Group,
agentService *agent.Service,
userService *user.Service,
middlewares ...echo.MiddlewareFunc) *echo.Group {
agentHandler := &AgentHandler{
agentService: agentService,
userService: userService,
}
agent := e.Group(prefix, middlewares...)
agent.GET("/:space", agentHandler.ListAgents)
agent.GET("/:space/:agent", agentHandler.GetAgent)
agent.PUT("/:space", agentHandler.UpdateAgent)
agent.POST("/:space", agentHandler.CreateAgent)
agent.DELETE("/:space/:agent", agentHandler.DeleteAgent)
return agent
}
type AgentHandler struct {
agentService *agent.Service
userService *user.Service
}
func (h *AgentHandler) ListAgents(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
agents, err := h.agentService.ListAgents(userID, teamID)
if err != nil {
c.Logger().Error("error listing agents", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing agents")
}
return c.JSON(http.StatusOK, agents)
}
func (h *AgentHandler) GetAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
agentID := c.Param("agent")
agent, err := h.agentService.GetAgent(userID, teamID, agentID)
if err != nil {
c.Logger().Error("error getting agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting agent")
}
return c.JSON(http.StatusOK, agent)
}
func (h *AgentHandler) CreateAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var agent domain.Agent
err := c.Bind(&agent)
if err != nil {
c.Logger().Error("error binding agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding agent")
}
agent.TeamID = teamID
updagent, err := h.agentService.CreateAgent(userID, &agent)
if err != nil {
c.Logger().Error("error creating agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating agent")
}
return c.JSON(http.StatusOK, updagent)
}
func (h *AgentHandler) UpdateAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var agent domain.Agent
err := c.Bind(&agent)
if err != nil {
c.Logger().Error("error binding agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding agent")
}
agent.TeamID = teamID
updagent, err := h.agentService.UpdateAgent(userID, &agent)
if err != nil {
c.Logger().Error("error updating agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error updating agent")
}
return c.JSON(http.StatusOK, updagent)
}
func (h *AgentHandler) DeleteAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
agentID := c.Param("agent")
err := h.agentService.DeleteAgent(userID, teamID, agentID)
if err != nil {
c.Logger().Error("error creating agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating agent")
}
return c.JSON(http.StatusOK, agentID)
}
type DefaulAgentRequest struct {
AgentID string `json:"AgentId"`
}
type DefaulAgentResponse struct {
NewDefaultAgentID *string `json:"newDefaultAgentId"`
OldDefaultAgentID *string `json:"oldDefaultAgentId"`
}
+72 -1
View File
@@ -17,6 +17,8 @@ func NewAuthGroup(prefix string, e *echo.Group, service *auth.Service) *echo.Gro
}
auth := e.Group(prefix)
auth.POST("/login", authHandler.Login)
auth.GET("/oidc/providers", authHandler.OIDCProviders)
auth.GET("/oidc/login", authHandler.OIDCLogin)
auth.GET("/oidc/callback", authHandler.OIDCCallback)
auth.POST("/register", authHandler.Register)
return auth
@@ -69,8 +71,77 @@ func (h AuthHandler) Login(c *echo.Context) error {
return c.JSON(http.StatusOK, resp)
}
// oidcRedirectURI reconstruit l'URL de callback du serveur à partir de la
// requête. Elle doit être identique entre /oidc/login et /oidc/callback.
func oidcRedirectURI(c *echo.Context) string {
scheme := "http"
if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto != "" {
scheme = proto
} else if c.Request().TLS != nil {
scheme = "https"
}
host := c.Request().Header.Get("X-Forwarded-Host")
if host == "" {
host = c.Request().Host
}
return scheme + "://" + host + "/api/auth/oidc/callback"
}
func (h AuthHandler) OIDCProviders(c *echo.Context) error {
providers, err := h.authService.ListOIDCProviders()
if err != nil {
c.Logger().Error(fmt.Sprintf("error listing oidc providers: %s", err))
return echo.NewHTTPError(http.StatusInternalServerError, "error listing oidc providers")
}
return c.JSON(http.StatusOK, providers)
}
func (h AuthHandler) OIDCLogin(c *echo.Context) error {
providerID := c.QueryParam("provider")
if providerID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "provider query parameter is required")
}
authorizeURL, err := h.authService.OIDCAuthorizeURL(context.Background(), providerID, oidcRedirectURI(c))
if err != nil {
c.Logger().Error(fmt.Sprintf("error building oidc authorize url: %s", err))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
return c.Redirect(http.StatusFound, authorizeURL)
}
func (h AuthHandler) OIDCCallback(c *echo.Context) error {
return nil
if errParam := c.QueryParam("error"); errParam != "" {
c.Logger().Error(fmt.Sprintf("oidc provider returned an error: %s", errParam))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
code := c.QueryParam("code")
state := c.QueryParam("state")
if code == "" || state == "" {
return echo.NewHTTPError(http.StatusBadRequest, "code and state are required")
}
session, err := h.authService.OIDCCallback(context.Background(), state, code, oidcRedirectURI(c))
if err != nil {
c.Logger().Error(fmt.Sprintf("error while oidc callback: %s", err))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
// L'application SPA est servie par ce même serveur (même origine) et lit
// ses tokens dans localStorage: on les dépose ici puis on redirige vers
// l'application, sans jamais les faire transiter dans une URL.
page := fmt.Sprintf(`<!doctype html>
<html>
<head><meta charset="utf-8"><title>Connexion en cours...</title></head>
<body>
<script>
localStorage.setItem("accessToken", %q);
localStorage.setItem("refreshToken", %q);
location.replace("/");
</script>
</body>
</html>`, session.AccessToken, session.RefreshToken)
return c.Blob(http.StatusOK, "text/html; charset=utf-8", []byte(page))
}
func (h AuthHandler) Register(c *echo.Context) error {
+192
View File
@@ -0,0 +1,192 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"strconv"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/chat"
)
func NewChatGroup(prefix string, e *echo.Group, service *chat.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
chatHandler := &ChatHandler{
chatService: service,
}
auth := e.Group(prefix, middlewares...)
auth.POST("/:space", chatHandler.NewChat)
auth.POST("/:space/:id", chatHandler.NewChatMessage)
auth.GET("/:space/all/:page", chatHandler.List)
auth.GET("/:space/:id", chatHandler.Get)
auth.GET("/:space/:id/subscribe", chatHandler.Subscribe)
auth.DELETE("/:space/:id", chatHandler.Delete)
return auth
}
type ChatHandler struct {
chatService *chat.Service
}
type ChatRequest struct {
Prompt string `json:"prompt"`
AgentID string `json:"agentID"`
ProviderID string `json:"providerID"`
ModelID string `json:"modelID"`
ReasoningLevel string `json:"reasoningLevel"`
}
func (h *ChatHandler) NewChat(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var request ChatRequest
err := c.Bind(&request)
if err != nil {
c.Logger().Error("error binding request", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error binding request")
}
chat, err := h.chatService.NewChat(
userID,
teamID,
request.AgentID,
"",
)
if err != nil {
c.Logger().Error("error creating chat", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error creating chat")
}
return c.JSON(http.StatusOK, chat)
}
func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
ctx := context.Background()
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
var request ChatRequest
err := c.Bind(&request)
if err != nil {
c.Logger().Error("error binding request", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error binding request")
}
enc := json.NewEncoder(c.Response())
chunkChan := make(chan *domain.Chunk)
_, err = h.chatService.AddChatMessage(
ctx,
userID,
teamID,
chatID,
request.AgentID,
request.Prompt,
&domain.ChatParams{
OnChunk: func(chunk *domain.Chunk) {
chunkChan <- chunk
},
},
)
if err != nil {
c.Logger().Error("error creating chat message", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error creating chat message")
}
enc.Encode(&domain.Chunk{
Done: false,
Role: string(domain.RoleUser),
Content: request.Prompt,
})
http.NewResponseController(c.Response()).Flush()
for {
chunk := <-chunkChan
enc.Encode(chunk)
http.NewResponseController(c.Response()).Flush()
if chunk.Done {
return nil
}
}
}
func (h *ChatHandler) List(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
page := c.Param("page")
pageInt, _ := strconv.Atoi(page)
query := c.QueryParam("q")
taskID := c.QueryParam("task")
chats, err := h.chatService.ListChats(userID, teamID, pageInt, query, taskID)
if err != nil {
c.Logger().Error("error listing chats", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing chats")
}
// évite "null" dans la réponse quand il n'y a pas de conversation
if chats == nil {
chats = []*domain.Chat{}
}
return c.JSON(http.StatusOK, chats)
}
func (h *ChatHandler) Get(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
chatWithMessages, err := h.chatService.GetChatWithMessages(userID, teamID, chatID)
if err != nil {
c.Logger().Error("error getting chat with messages", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting chat")
}
return c.JSON(http.StatusOK, chatWithMessages)
}
func (h *ChatHandler) Delete(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
err := h.chatService.DeleteChat(userID, teamID, chatID)
if err != nil {
c.Logger().Error("error deleting chat", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error deleting chat")
}
return c.JSON(http.StatusOK, "chat deleted")
}
func (h *ChatHandler) Subscribe(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
enc := json.NewEncoder(c.Response())
chunkChan := make(chan *domain.Chunk)
err := h.chatService.Subscribe(
userID,
teamID,
chatID,
func(chunk *domain.Chunk) {
chunkChan <- chunk
},
)
if err != nil {
c.Logger().Error("error subscribing to chat", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error creating chat")
}
for {
chunk := <-chunkChan
if chunk.Done {
return nil
}
enc.Encode(chunk)
http.NewResponseController(c.Response()).Flush()
}
}
+22 -10
View File
@@ -17,6 +17,7 @@ func NewModelGroup(prefix string, e *echo.Group, service *provider.Service, midd
model.GET("/providerTypes", modelHandler.ListProviderTypes)
model.GET("/:space", modelHandler.ListProviders)
model.GET("/:space/models", modelHandler.ListProvidersModels)
model.POST("/:space/avalable-models", modelHandler.ListAvailableModels)
model.GET("/:space/:provider", modelHandler.GetProvider)
model.PUT("/:space", modelHandler.UpdateProvider)
@@ -39,7 +40,7 @@ func (h *ModelHandler) ListProviders(c *echo.Context) error {
teamID := c.Param("space")
providers, err := h.providerService.ListProviders(userID, teamID)
if err != nil {
c.Logger().Error("error listing providers: %s", err)
c.Logger().Error("error listing providers", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing providers")
}
return c.JSON(http.StatusOK, providers)
@@ -51,7 +52,7 @@ func (h *ModelHandler) GetProvider(c *echo.Context) error {
providerID := c.Param("provider")
provider, err := h.providerService.GetProvider(userID, teamID, providerID)
if err != nil {
c.Logger().Error("error getting provider: %s", err)
c.Logger().Error("error getting provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting provider")
}
return c.JSON(http.StatusOK, provider)
@@ -63,13 +64,13 @@ func (h *ModelHandler) CreateProvider(c *echo.Context) error {
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider: %s", err)
c.Logger().Error("error binding provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
updprovider, err := h.providerService.CreateProvider(userID, &provider)
if err != nil {
c.Logger().Error("error creating provider: %s", err)
c.Logger().Error("error creating provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, updprovider)
@@ -81,13 +82,13 @@ func (h *ModelHandler) UpdateProvider(c *echo.Context) error {
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider: %s", err)
c.Logger().Error("error binding provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
updprovider, err := h.providerService.UpdateProvider(userID, &provider)
if err != nil {
c.Logger().Error("error updating provider: %s", err)
c.Logger().Error("error updating provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, updprovider)
@@ -99,7 +100,7 @@ func (h *ModelHandler) DeleteProvider(c *echo.Context) error {
providerID := c.Param("provider")
err := h.providerService.DeleteProvider(userID, teamID, providerID)
if err != nil {
c.Logger().Error("error creating provider: %s", err)
c.Logger().Error("error creating provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, providerID)
@@ -110,14 +111,25 @@ func (h *ModelHandler) ListAvailableModels(c *echo.Context) error {
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider: %s", err)
c.Logger().Error("error binding provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
list, err := h.providerService.ListAvailableModels(c.Request().Context(), &provider)
if err != nil {
c.Logger().Error("error listing models: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
c.Logger().Error("error listing available models", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing available models")
}
return c.JSON(http.StatusOK, list)
}
func (h *ModelHandler) ListProvidersModels(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
list, err := h.providerService.ListProvidersModels(userID, teamID)
if err != nil {
c.Logger().Error("error listing providers models", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing providers models")
}
return c.JSON(http.StatusOK, list)
}
+70
View File
@@ -0,0 +1,70 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/task"
)
type TaskHandler struct {
taskService *task.Service
}
func NewTaskGroup(prefix string, e *echo.Group, service *task.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
taskHandler := &TaskHandler{taskService: service}
auth := e.Group(prefix, middlewares...)
auth.GET("/:space", taskHandler.List)
auth.PUT("/:space/:id", taskHandler.Update)
auth.DELETE("/:space/:id", taskHandler.Delete)
return auth
}
func (h *TaskHandler) List(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
tasks, err := h.taskService.ListTasks(userID, teamID)
if err != nil {
c.Logger().Error("error listing tasks", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing tasks")
}
// évite "null" dans la réponse quand il n'y a pas de tâche
if tasks == nil {
tasks = []*domain.Task{}
}
return c.JSON(http.StatusOK, tasks)
}
func (h *TaskHandler) Update(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
taskID := c.Param("id")
var taskModel domain.Task
if err := c.Bind(&taskModel); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "error binding request")
}
taskModel.ID = taskID
updated, err := h.taskService.UpdateTask(userID, teamID, &taskModel)
if err != nil {
c.Logger().Error("error updating task", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error updating task")
}
return c.JSON(http.StatusOK, updated)
}
func (h *TaskHandler) Delete(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
taskID := c.Param("id")
if err := h.taskService.DeleteTask(userID, teamID, taskID); err != nil {
c.Logger().Error("error deleting task", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error deleting task")
}
return c.JSON(http.StatusOK, "task deleted")
}
+55
View File
@@ -0,0 +1,55 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewTeamGroup(prefix string, e *echo.Group, service *user.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
teamHandler := &TeamHandler{
userService: service,
}
h := e.Group(prefix, middlewares...)
h.GET("/:team", teamHandler.Get)
h.PUT("/:team", teamHandler.Update)
return h
}
type TeamHandler struct {
userService *user.Service
}
func (h TeamHandler) Get(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("team")
user, err := h.userService.GetTeam(userID, teamID)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
}
return c.JSON(http.StatusOK, user)
}
func (h TeamHandler) Update(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("team")
var updTeam domain.Team
if err := c.Bind(&updTeam); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
if teamID != updTeam.ID {
return c.String(http.StatusBadRequest, "bad request")
}
user, err := h.userService.UpdateTeam(userID, &updTeam)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, user)
}
+123
View File
@@ -0,0 +1,123 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewToolGroup(
prefix string,
e *echo.Group,
ToolService *tool.Service,
userService *user.Service,
middlewares ...echo.MiddlewareFunc) *echo.Group {
ToolHandler := &ToolHandler{
ToolService: ToolService,
userService: userService,
}
Tool := e.Group(prefix, middlewares...)
Tool.GET("/:space", ToolHandler.ListTools)
Tool.GET("/:space/:tool", ToolHandler.GetTool)
Tool.GET("/:space/:tool/params", ToolHandler.GetToolParams)
Tool.PUT("/:space", ToolHandler.UpdateTool)
Tool.POST("/:space", ToolHandler.CreateTool)
//Tool.DELETE("/:space/:tool", ToolHandler.DeleteTool)
return Tool
}
type ToolHandler struct {
ToolService *tool.Service
userService *user.Service
}
func (h *ToolHandler) ListTools(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
Tools, err := h.ToolService.ListTools(userID, teamID)
if err != nil {
c.Logger().Error("error listing Tools", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing Tools")
}
return c.JSON(http.StatusOK, Tools)
}
func (h *ToolHandler) GetTool(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
ToolID := c.Param("tool")
Tool, err := h.ToolService.GetTool(userID, teamID, ToolID)
if err != nil {
c.Logger().Error("error getting Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting Tool")
}
return c.JSON(http.StatusOK, Tool)
}
func (h *ToolHandler) GetToolParams(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
toolID := c.Param("tool")
tool, err := h.ToolService.GetTool(userID, teamID, toolID)
if err != nil {
c.Logger().Error("error getting Tool", "error", err, "teamid", teamID, "toolID", toolID)
return echo.NewHTTPError(http.StatusBadRequest, "error getting Tool")
}
toolImpl := h.ToolService.GetToolImpl(tool.Name)
return c.JSON(http.StatusOK, toolImpl.Params())
}
func (h *ToolHandler) CreateTool(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var Tool domain.Tool
err := c.Bind(&Tool)
if err != nil {
c.Logger().Error("error binding Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding Tool")
}
Tool.TeamID = teamID
updTool, err := h.ToolService.CreateTool(userID, &Tool)
if err != nil {
c.Logger().Error("error creating Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating Tool")
}
return c.JSON(http.StatusOK, updTool)
}
func (h *ToolHandler) UpdateTool(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var Tool domain.Tool
err := c.Bind(&Tool)
if err != nil {
c.Logger().Error("error binding Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding Tool")
}
Tool.TeamID = teamID
updTool, err := h.ToolService.UpdateTool(userID, &Tool)
if err != nil {
c.Logger().Error("error updating Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error updating Tool")
}
return c.JSON(http.StatusOK, updTool)
}
// func (h *ToolHandler) DeleteTool(c *echo.Context) error {
// userID := c.Get(auth.ContextUserIDKey).(string)
// teamID := c.Param("space")
// ToolID := c.Param("tool")
// err := h.ToolService.DeleteTool(userID, teamID, ToolID)
// if err != nil {
// c.Logger().Error("error creating Tool", "error", err)
// return echo.NewHTTPError(http.StatusBadRequest, "error creating Tool")
// }
// return c.JSON(http.StatusOK, ToolID)
// }
+28 -4
View File
@@ -1,6 +1,7 @@
package http
import (
"context"
"embed"
"fmt"
"io/fs"
@@ -10,9 +11,13 @@ import (
"github.com/labstack/echo/v5/middleware"
"trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/http/handlers"
"trankilou.fr/lassistanoque/backend/internal/service/agent"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/chat"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/task"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
@@ -29,7 +34,7 @@ var indexhtml []byte
var robotstxt []byte
type Router struct {
echo *echo.Echo
server *http.Server
}
type Dependencies struct {
@@ -37,11 +42,17 @@ type Dependencies struct {
AuthService *auth.Service
UserService *user.Service
ProviderService *provider.Service
AgentService *agent.Service
ChatService *chat.Service
ToolService *tool.Service
TaskService *task.Service
TokenManager auth.TokenManager
}
func NewRouter(deps Dependencies) *Router {
cfg := config.GetConfig()
e := echo.New()
e.Use(middleware.RequestLogger())
e.Use(middleware.CORS("http://localhost:5173"))
@@ -74,13 +85,26 @@ func NewRouter(deps Dependencies) *Router {
_ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware)
_ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewToolGroup("/tool", api, deps.ToolService, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewTeamGroup("/team", api, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewChatGroup("/chat", api, deps.ChatService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewTaskGroup("/task", api, deps.TaskService, deps.TokenManager.TokenMiddleware)
server := http.Server{
Addr: fmt.Sprintf(":%d", cfg.HttpPort),
Handler: e,
}
return &Router{
echo: e,
&server,
}
}
func (r *Router) Start() error {
cfg := config.GetConfig()
return r.echo.Start(fmt.Sprintf(":%d", cfg.HttpPort))
return r.server.ListenAndServe()
}
func (r *Router) Stop(ctx context.Context) error {
return r.server.Shutdown(ctx)
}
@@ -0,0 +1,2 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-duration:initial;--tw-leading:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-green-300:oklch(87.1% .15 154.449);--color-green-500:oklch(72.3% .219 149.579);--color-green-700:oklch(52.7% .154 150.069);--color-cyan-600:oklch(60.9% .126 221.723);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-700:oklch(50% .134 242.749);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-900:oklch(37.9% .146 265.522);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-bold:700;--radius-sm:.25rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-heightLine truncated
@@ -1,2 +0,0 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-outline-style:solid;--tw-font-weight:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-700:oklch(50% .134 242.749);--color-sky-950:oklch(29.3% .066 243.157);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-bold:700;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiLine truncated
@@ -0,0 +1 @@
img.svelte-1hoglcg{opacity:0;max-width:100%;height:auto}img.fade-in.svelte-1hoglcg{opacity:1;transition:opacity .3s ease-in-out}img.visible.svelte-1hoglcg{opacity:1;transition:none}img.error.svelte-1hoglcg{opacity:.5;filter:grayscale()}
@@ -0,0 +1 @@
import{A as e,B as t,K as n,M as r,N as i,Q as a,W as o,Z as s,at as c,dt as l,ft as u,it as d,j as f,k as p,nt as m,rt as h,tt as g,vt as _}from"./DGrvnoUE.js";import"./xihTtKlq.js";var v=class extends Map{#e=new Map;#t=d(0);#n=d(0);#r=o||-1;constructor(e){if(super(),e){for(var[t,n]of e)super.set(t,n);this.#n.v=super.size}}#i(e){return o===this.#r?d(e):h(e)}has(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else return t(this.#t),!1;return t(r),!0}forEach(e,t){this.#a(),super.forEach(e,t)}get(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else{t(this.#t);return}return t(r),super.get(e)}set(e,t){var n=this.#e,r=n.get(e),i=super.get(e),a=super.set(e,t),o=this.#t;if(r===void 0)r=this.#i(0),n.set(e,r),m(this.#n,super.size),g(o);else if(i!==t){g(r);var s=o.reactions===null?null:new Set(o.reactions);(s===null||!r.reactions?.every(e=>s.has(e)))&&g(o)}return a}delete(e){var t=this.#e,n=t.get(e),r=super.delete(e);return n!==void 0&&(t.delete(e),m(n,-1)),r&&(m(this.#n,super.size),g(this.#t)),r}clear(){if(super.size!==0){super.clear();var e=this.#e;m(this.#n,0);for(var t of e.values())m(t,-1);g(this.#t),e.clear()}}#a(){t(this.#t);var e=this.#e;if(this.#n.v!==e.size){for(var n of super.keys())if(!e.has(n)){var r=this.#i(0);e.set(n,r)}}for([,r]of this.#e)t(r)}keys(){return t(this.#t),super.keys()}values(){return this.#a(),super.values()}entries(){return this.#a(),super.entries()}[Symbol.iterator](){return this.entries()}get size(){return t(this.#n),super.size}},y=i(`<span class="text-red-700 dark:text-red-400 text-xs"> </span>`);function b(i,o){u(o,!0);var d=r(),m=a(d),h=t=>{var r=y(),i=s(r,!0);_(r),n(t=>e(i,t),[()=>o.errors.get(o.key)]),f(t,r)},g=c(()=>o.errors.has(o.key));p(m,e=>{t(g)&&e(h)}),f(i,d),l()}export{v as n,b as t};
@@ -0,0 +1 @@
import{gt as e}from"./DGrvnoUE.js";e();
@@ -1 +0,0 @@
import"./DxmhE1z1.js";import{a as e,r as t}from"./DZTG_XPZ.js";var n={get data(){return e.data},get error(){return e.error},get form(){return e.form},get params(){return e.params},get route(){return e.route},get state(){return e.state},get status(){return e.status},get url(){return e.url}};t.updated.check;var r=n;export{r as t};
@@ -0,0 +1 @@
var e=`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXoAAAA1CAYAAABP2SCdAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAClxJREFUeJzt3W2MXFUZB/D/c+6d7WK7c6e73btbWgSUlshrlQ8YAoKQIIkhBk2lO01rEz6YUrTxJTRqsKyaygfxLUgUTKi4c7dQEU2IiIIaRAOUtxZTlZgUQdqdu9vuzvR9Z+Y8fmi7vXdmdu/c05md5fL8PvWePec8596982x67pxziZkhhBAiuVS7ByCEEKK17FP/WDg0ch2T6pipMpOeKGT7X2z9sIRozPxtY2enKvqSYBlZ+o3xVf1vtmlIQsw5U4leK/UYgO6Zq6sXAVzZ2iEJ0ThL841MeChUyNbXANzTnhEJMffI1I0QQiScJHohhEg4SfRCCJFwU3P0RPg5M+ZPHYM6GXxbe4YlWiE9nL+ZWJ0bLFOTqaHxdZmJdo1JCNF6U4l+YsDdFPxB1/aRXlVSkugThDTdDuKbgmWVeZWnAUiiFyLBZOpGCCESThK9EEIknCR6IYRIODu6ikgKTXiQgGeCZWQf9ds1HiHE7JBE/x5yMOv+ut1jEELMPpm6EUKIhJNEL4QQCSeJXgghEq6tc/SO598H0Pvjt+RXCln3bqOYudHvgHBZ/JZ6TyHbt9EkZtrzv0qgj8VvyeOFrPs5s5j52wjqU1H1SmW1/sjanndMYlTLDPufYSaj8arJ1NqoFbrpR8aWU4W/Fywj8DnV9VhjreONXhUZU1W+aLKdcWZo7COs+O647U7ExObxVb2vzlSHtu/uSJd6f2XSf92YqPxgPNv/57jtMl5hIWPyFyYxNXiryTMhAijtjf7WJGbd/pjvm1jt/iFYlh7ObyFWoa2trZS95sDKhYW4/XcNj12rmL8SikkYmhjofTSqrZPL/xikzosbE8DOQrb3rjgN2vswlul6EH/IoOU885C4isAfj90OtMs0JoGuAPhmg6Z545ikLgFHx7RTuNM0Rp2oywzPE6X5pc7I3suVbhBF93/inoq+r8oq1oflFLbYbeTa1qM1fhJdK22ZXse6MaGM/miUdKnTVmbjIMZzJu0wCMKy5p07CE/UFtHVAF8TLCsdmzTKKYr1UgCh8TJjR0NDI7qOwZfGjck4vVVNo2TqRgghEk4SvRBCJJwkeiGESLi2ztGT1p/Uth17boy4vMLx/H+FysAPT2T7tgTLnGH/m2Bkg2UKdJcm6/a4MS0qHWuknpMb3QDiL4THy/dqZQ/GjUkVXY7b5hRd1t8l2/5ZqD9UHgDjmunaxJHJ5W9gotB8M4MeA1kmz1xwGD2jUXWKpaOvdc3rCvWvtL6FibdUVf0+k/VgVH8TPeN7ADfmSIGzyvqvR2zb6DyJ9WbH838ULCsrdf3hVYv2ztiQ6Z+s1KfNYlY2Op7/9WCZRfjsgQE39NzJ8fI7AOo6dWwrHGDD3yeovLb6M6qg11c/FHZy/pMgnH/qOH0BaeOYXFlHwKboirPP8UZ/CIR3jgWwkcn6b9y+FPRl1dcWQK6Qdb89XZu2JvqJNX17TNo5nn8ugAuDZQz01VRk9FfXA+n9xYHe6ovUPMSLasamcKg40NO6mHUcXNPnAwhtb+Dk/MOg5vSvFXURV11bsNXK8+R15x0DEOo/7Y3uqz4lIhotNDSOHqNxjKzpO1w9jkY5nt+NqvujQ5dSkQ2Jj5teW8fz51fHrBCfVSfIcgDpqUPGSDFrFjOT822mqpj69B+R0yHxQQDLTh8zm55netj3wSYtW4+Jl1R/XizCvgMG55rJ5ZeAKPzZO5HrpiVTN0IIkXCS6IUQIuEk0QshRMK1e2XsPwBc1Kr+i1l3AwZxR7Csa5n/R8fznzbobmch6344qlIh6w7SIEIPRdIX+EOO5+cMYo4Usu7ZBu1arjjg/oYGYQXLnOWjdzqer036qxAWHxpwjReIzSbH8z8B4EnD5k16SiLe7SqM1xzPYJdwin8LtXtlrAJxy258Bhibw49n0h4RED8mx/jfD29GKNk5hjExx//HVX2emWEAponMNrh724WIwK27b8V7xqzdQ3M6kQghhDhzkuiFECLhZm3qJv3wOz2pDnpfqFDZY8z4X9y+COgEsMhkHAT4jPgxAdrfs21vaKfE0iQfKa5dsj+6Le83ikmoiTlpqWMHV/ZHLi5qBwYXza4tkDrOfT3b9k59n/zo5DzdrF01zwRth9Vd2Rt6TkI6tUCT2XnSifs2cgM3MTPa+mZnd2dHb6iM7UxLYz6AVHd6b/j76pTqBht/eT/PQOmMBwaAQOMz/XzWEr1KpR4sa9wSLNOsLz64un933L5OPgz7vck4CtneVSbtFg75l5dhvxUsI4sfBXBrdEz3DiD8ULgRC7aP9pdL9r5gmaXxLIBr4/Y1GwoD7v0A7jdp63j+iNb21KK3lF0pAGjpB7cRmcrIOWVthxf2ET9VzLo12yM3wvH8pwDc2IyxvZelOxfcUNa6ZmfKVnIWjF1W1vZL4VLzFVoW4cbq1cmtIlM3QgiRcJLohRAi4STRCyFEwk07R5/CvMkySn8KlhHwTiaXvyGqUyL9n/Hs4tCubMzYxYATLLNAl2Zy+cVxB02kVnADc2PpobELFVWWxu2/HrbgMlP4ehBer66X+WX+fCh8oBkxFWM+q3BMADub0fcU1h/N5PJLgkUTq/ueiWqWHt63SGl1edPGoegFZiw4dUiEQw01Y96rCeFrxNpu5D7t6FAv+Ct7Z4xTYnVUoap/wluN9F8XoXvOrpki/CX0O1A4ZHqeDDXJ4PB1s6j2SwSEvzPj7dOH4MZyDC3RVb8XAs5BcIO06Vu/zODQQ1CycWUmlz8yUysm6ufamH0ALo6MCLzOQHewrMJ8USaX752uTRzatt4u3rrojel+Pm2iP/n+xNAFd7blr2RNz0eHVd8AENo6tt47Xp3c6G6zVwk29gCElN7IoPXx+68TkbGrmHUjExtbtBbA3c2ISYR8IevOuCvdGccAP8S1a5UayETW1Ux4vFnjqNi0+NDK3pG47U6+DzT8TlDPvweEyNXPpeO8AhF/OE+u1g1/DoZHb+IG+n+3KQy4oXcMzx8aW2wrPfP2ydNg5k3FrBuZsAsD7rrgMQ1CpZf5lQYi/K6Y7Qv1nx72v0yMe6Nj9n6puszxfJ8JUUn3lWLWvaKq3WoAQ9Ex3W9Vl2U8fxcTYr9KsB4q6/sBbJju5zJ1I4QQCSeJXgghEk4SvRBCJFysBVMKnNegn0bVY9DLjXXI28CI/TC2HoJ6rs5InjXa6q0ORdzgSkjaAXDkNWoEA4Vm9HMaPQHwm83oyapgj1ZoynkCwLxJa8YHYbEQngdHj+24tsZMute68pYi1bRz16nKwVDB+NIyFvih/gn8Ngwx0zNEfDhYpsCRO4V2kjpchjY6T6Xwqkk7bAbDa+C+Iqr5IsSZIMZWJtS+ASsUkmpf+8f6DVTfC0wv1dSrH/QRMP0txjCn7wr87MyhzJfvCiHEnFHvYSwBn5/Iug+0a0xzhUzdCCFEwkmiF0KIhJNEL4QQCSeJXgghEk4SvRBCJJwkeiGESDhJ9EIIkXCS6IUQIuFm7VWCQgjRSqrMj3PK+newzMLkrLyqb66TlbFCCJFw/weRJ1LNZA9BxAAAAABJRU5ErkJggg==`;export{e as t};
@@ -1 +0,0 @@
import{B as e,C as t,D as n,F as r,H as i,K as a,P as o,Q as s,R as c,V as l,_ as u,g as d,h as f,k as p,m,q as h,v as g,z as _}from"./DxmhE1z1.js";import"./xihTtKlq.js";var v=class extends Map{#e=new Map;#t=l(0);#n=l(0);#r=n||-1;constructor(e){if(super(),e){for(var[t,n]of e)super.set(t,n);this.#n.v=super.size}}#i(t){return n===this.#r?l(t):e(t)}has(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else return t(this.#t),!1;return t(r),!0}forEach(e,t){this.#a(),super.forEach(e,t)}get(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else{t(this.#t);return}return t(r),super.get(e)}set(e,t){var n=this.#e,r=n.get(e),i=super.get(e),a=super.set(e,t),o=this.#t;if(r===void 0)r=this.#i(0),n.set(e,r),_(this.#n,super.size),c(o);else if(i!==t){c(r);var s=o.reactions===null?null:new Set(o.reactions);(s===null||!r.reactions?.every(e=>s.has(e)))&&c(o)}return a}delete(e){var t=this.#e,n=t.get(e),r=super.delete(e);return n!==void 0&&(t.delete(e),_(n,-1)),r&&(_(this.#n,super.size),c(this.#t)),r}clear(){if(super.size!==0){super.clear();var e=this.#e;_(this.#n,0);for(var t of e.values())_(t,-1);c(this.#t),e.clear()}}#a(){t(this.#t);var e=this.#e;if(this.#n.v!==e.size){for(var n of super.keys())if(!e.has(n)){var r=this.#i(0);e.set(n,r)}}for([,r]of this.#e)t(r)}keys(){return t(this.#t),super.keys()}values(){return this.#a(),super.values()}entries(){return this.#a(),super.entries()}[Symbol.iterator](){return this.entries()}get size(){return t(this.#n),super.size}},y=g(`<span class="text-red-700 dark:text-red-400 text-xs"> </span>`),b=g(`<span class="text-xs">&nbsp;</span>`);function x(e,n){h(n,!0);var c=u(),l=r(c),g=e=>{var t=y(),r=o(t,!0);s(t),p(e=>f(r,e),[()=>n.errors.get(n.key)]),d(e,t)},_=i(()=>n.errors.has(n.key)),v=e=>{var t=b();d(e,t)};m(l,e=>{t(_)?e(g):e(v,-1)}),d(e,c),a()}export{v as n,x as t};
@@ -0,0 +1 @@
var e=e=>{e.focus()};export{e as t};
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={createChat:(t,n)=>e.post(`/chat/${t}`,n),createChatMessage:(t,n,r)=>e.postStream(`/chat/${t}/${n}`,r),allChats:(t,n,r,i)=>e.get(`/chat/${t}/all/${n}${r||i?`?${[r&&`q=${encodeURIComponent(r)}`,i&&`task=${i}`].filter(Boolean).join(`&`)}`:``}`),getChat:(t,n)=>e.get(`/chat/${t}/${n}`),subscribe:(t,n)=>e.get(`/chat/${t}/${n}/subscribe`),deleteChat:(t,n)=>e.delete(`/chat/${t}/${n}`)};export{t};
@@ -0,0 +1 @@
import{B as e,H as t,V as n,ct as r,it as i,nt as a,r as o,t as s}from"./DGrvnoUE.js";var c=class{constructor(e,t){this.status=e,this.body=typeof t==`string`?{message:t}:t||{message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}},l=class{constructor(e,t){try{new Headers({location:t})}catch{throw Error(`Invalid redirect location ${JSON.stringify(t)}: this string contains characters that cannot be used in HTTP headers`)}this.status=e,this.location=t}},u=class extends Error{constructor(e,t,n){super(n),this.status=e,this.text=t}};new URL(`sveltekit-internal://`);function d(e,t){return e===`/`||t===`ignore`?e:t===`never`?e.endsWith(`/`)?e.slice(0,-1):e:t===`always`&&!e.endsWith(`/`)?e+`/`:e}function f(e){return e.split(`%25`).map(decodeURI).join(`%25`)}function p(e){for(let t in e)e[t]=decodeURIComponent(e[t]);return e}function m({href:e}){return e.split(`#`)[0]}function h(e,t,n,r=!1){let i=new URL(e);Object.defineProperty(i,"searchParams",{value:new Proxy(i.searchParams,{get(e,r){if(r===`get`||r===`getAll`||r===`has`)return(t,...i)=>(n(t),e[r](t,...i));t();let i=Reflect.get(e,r);return typeof i==`function`?i.bind(e):i}}),enumerable:!0,configurable:!0});let a=[`href`,`pathname`,`search`,`toString`,`toJSON`];r&&a.push(`hash`);for(let n of a)Object.defineProperty(i,n,{get(){return t(),e[n]},enumerable:!0,configurable:!0});return i}function g(){}function _(...e){let t=5381;for(let n of e)if(typeof n==`string`){let e=n.length;for(;e;)t=t*33^n.charCodeAt(--e)}else if(ArrayBuffer.isView(n)){let e=new Uint8Array(n.buffer,n.byteOffset,n.byteLength),r=e.length;for(;r;)t=t*33^e[--r]}else throw TypeError(`value must be a string or TypedArray`);return(t>>>0).toString(36)}new TextEncoder;function v(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}var ee=window.fetch;window.fetch=(e,t)=>((e instanceof Request?e.method:t?.method||`GET`)!==`GET`&&y.delete(x(e)),ee(e,t));var y=new Map;function b(e,t){let n=x(e,t),r=document.querySelector(n);if(r?.textContent){r.remove();let{body:e,...t}=JSON.parse(r.textContent);r.getAttribute(`data-b64`)!==null&&(e=v(e));let i=r.getAttribute(`data-ttl`);return i&&y.set(n,{body:e,init:t,ttl:1e3*Number(i)}),Promise.resolve(new Response(e,t))}return window.fetch(e,t)}function te(e,t,n){if(y.size>0){let t=x(e,n),r=y.get(t);if(r){if(performance.now()<r.ttl&&[`default`,`force-cache`,`only-if-cached`,void 0].includes(n?.cache))return new Response(r.body,r.init);y.delete(t)}}return window.fetch(t,n)}function x(e,t){let n=`script[data-sveltekit-fetched][data-url=${JSON.stringify(e instanceof Request?e.url:e)}]`;if(t?.headers||t?.body){let e=[];t.headers&&e.push([...new Headers(t.headers)].join(`,`)),t.body&&(typeof t.body==`string`||ArrayBuffer.isView(t.body))&&e.push(t.body),n+=`[data-hash="${_(...e)}"]`}return n}var ne=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/,re=/^\/\((?:[^)]+)\)$/;function ie(e){let t=[];return{pattern:e===`/`||re.test(e)?/^\/$/:RegExp(`^${oe(e).map(e=>{let n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(e);if(n)return t.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),`(?:/([^]*))?`;let r=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(e);if(r)return t.push({name:r[1],matcher:r[2],optional:!0,rest:!1,chained:!0}),`(?:/([^/]+))?`;if(!e)return;let i=e.split(/\[(.+?)\](?!\])/);return`/`+i.map((e,n)=>{if(n%2){if(e.startsWith(`x+`))return ce(String.fromCharCode(parseInt(e.slice(2),16)));if(e.startsWith(`u+`))return ce(String.fromCharCode(...e.slice(2).split(`-`).map(e=>parseInt(e,16))));let[,r,a,o,s]=ne.exec(e);return t.push({name:o,matcher:s,optional:!!r,rest:!!a,chained:a?n===1&&i[0]===``:!1}),a?`([^]*?)`:r?`([^/]*)?`:`([^/]+?)`}return ce(e)}).join(``)}).join(``)}/?$`),params:t}}function ae(e){return e!==``&&!/^\([^)]+\)$/.test(e)}function oe(e){return e.slice(1).split(`/`).filter(ae)}function se(e,t,n){let r={},i=e.slice(1),a=i.filter(e=>e!==void 0),o=0;for(let e=0;e<t.length;e+=1){let s=t[e],c=i[e-o];if(s.chained&&s.rest&&o&&(c=i.slice(e-o,e+1).filter(e=>e).join(`/`),o=0),c===void 0)if(s.rest)c=``;else continue;if(!s.matcher||n[s.matcher](c)){r[s.name]=c;let n=t[e+1],l=i[e+1];n&&!n.rest&&n.optional&&l&&s.chained&&(o=0),!n&&!l&&Object.keys(r).length===a.length&&(o=0);continue}if(s.optional&&s.chained){o++;continue}return}if(!o)return r}function ce(e){return e.normalize().replace(/[[\]]/g,`\\$&`).replace(/%/g,`%25`).replace(/\//g,`%2[Ff]`).replace(/\?/g,`%3[Ff]`).replace(/#/g,`%23`).replace(/[.*+?^${}()|\\]/g,`\\$&`)}function le({nodes:e,server_loads:t,dictionary:n,matchers:r}){let i=new Set(t);return Object.entries(n).map(([t,[n,i,s]])=>{let{pattern:c,params:l}=ie(t),u={id:t,exec:e=>{let t=c.exec(e);if(t)return se(t,l,r)},errors:[1,...s||[]].map(t=>e[t]),layouts:[0,...i||[]].map(o),leaf:a(n)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function a(t){let n=t<0;return n&&(t=~t),[n,e[t]]}function o(t){return t===void 0?t:[i.has(t),e[t]]}}function ue(e,t=JSON.parse){try{return t(sessionStorage Line truncated
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={providerTypes:()=>e.get(`/provider/providerTypes`),getProviders:t=>e.get(`/provider/${t}`),getProvider:(t,n)=>e.get(`/provider/${t}/${n}`),createProvider:(t,n)=>e.post(`/provider/${t}`,n),updateProvider:(t,n)=>e.put(`/provider/${t}`,n),deleteProvider:(t,n)=>e.delete(`/provider/${t}/${n}`),getAvailableModels:(t,n)=>e.post(`/provider/${t}/avalable-models`,n),getProvidersModels:t=>e.get(`/provider/${t}/models`)};export{t};
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={register:t=>e.post(`/auth/register`,t),status:()=>e.get(`/auth/status`),login:t=>e.post(`/auth/login`,t),oidcProviders:()=>e.get(`/auth/oidc/providers`,{auth:!1}),oidcLoginUrl:e=>`/api/auth/oidc/login?provider=${encodeURIComponent(e)}`};export{t};
@@ -0,0 +1,4 @@
import{$ as e,A as t,C as n,I as r,K as i,L as a,M as o,N as s,Q as c,R as l,X as u,Z as d,a as f,dt as p,et as m,ft as h,j as g,k as _,vt as v}from"./DGrvnoUE.js";import"./xihTtKlq.js";import{t as y}from"./DtfiK5fE.js";var b={listTasks:e=>y.get(`/task/${e}`),updateTask:(e,t,n)=>y.put(`/task/${e}/${t}`,n),deleteTask:(e,t)=>y.delete(`/task/${e}/${t}`)},x=0,S=m([]),C=(e,t)=>{S.push({id:x++,type:e,text:t,status:`appearing`})},w=e=>e;function T(e){let t=e-1;return t*t*t+1}function E(e,{delay:t=0,duration:n=400,easing:r=w}={}){let i=+getComputedStyle(e).opacity;return{delay:t,duration:n,easing:r,css:e=>`opacity: ${e*i}`}}function D(e,{delay:t=0,duration:n=400,easing:r=T,start:i=0,opacity:a=0}={}){let o=getComputedStyle(e),s=+o.opacity,c=o.transform===`none`?``:o.transform,l=1-i,u=s*(1-a);return{delay:t,duration:n,easing:r,css:(e,t)=>`
transform: ${c} scale(${1-l*t});
opacity: ${s-u*t}
`}}var O=s(`<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"></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"><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"> </h2></div> <p class="mt-3 text-sm text-stone-600 dark:text-stone-400"> </p> <div class="mt-4 flex justify-end gap-2"><button type="button" class="bt small"> </button> <button type="button" class="bt small danger"> </button></div></div></div>`);function k(r,s){h(s,!0);let m=f(s,`open`,3,!1),y=f(s,`title`,3,``),b=f(s,`message`,3,``),x=f(s,`confirmLabel`,3,`Confirmer`),S=f(s,`cancelLabel`,3,`Annuler`),C=()=>s.onCancel?.(),w=e=>{m()&&e.key===`Escape`&&C()};var T=o();l(`keydown`,u,w);var k=c(T),A=r=>{var o=O(),c=d(o),l=e(c,2),u=d(l),f=e(d(u),2),p=d(f,!0);v(f),v(u);var m=e(u,2),h=d(m,!0);v(m);var _=e(m,2),w=d(_),T=d(w,!0);v(w);var k=e(w,2),A=d(k,!0);v(k),v(_),v(l),v(o),i(()=>{t(p,y()),t(h,b()),t(T,S()),t(A,x())}),a(`click`,c,C),n(3,c,()=>E,()=>({duration:150})),a(`click`,w,C),a(`click`,k,function(...e){s.onConfirm?.apply(this,e)}),n(3,l,()=>D,()=>({duration:150,start:.95})),g(r,o)};_(k,e=>{m()&&e(A)}),g(r,T),p()}r([`click`]);export{C as n,b as r,k as t};
@@ -0,0 +1 @@
import"./BZuNOv8h.js";
@@ -0,0 +1 @@
import{et as e}from"./DGrvnoUE.js";var t=e({spaces:[],current:``});export{t};
@@ -0,0 +1 @@
var e=/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,t=/^https?:\/\/([a-zA-Z0-9.-]+|\[[0-9a-fA-F:.]+\])(:\d+)?(\/[^\s]*)?$/;function n(t){return e.test(t)}function r(e){return t.test(e)}export{r as n,n as t};
@@ -0,0 +1,3 @@
var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r},n=Array.isArray,r=Array.prototype.indexOf,i=Array.prototype.includes,a=Array.from,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyDescriptors,l=Object.prototype,u=Array.prototype,d=Object.getPrototypeOf,f=Object.isExtensible;function p(e){return typeof e==`function`}var m=()=>{};function h(e){for(var t=0;t<e.length;t++)e[t]()}function g(){var e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}var _=1<<24,v=1024,y=2048,b=4096,x=8192,S=16384,ee=32768,te=1<<25,ne=65536,re=1<<18,ie=1<<19,ae=1<<20,oe=1<<25,se=65536,ce=1<<21,le=1<<22,ue=1<<23,C=Symbol(`$state`),de=Symbol(`legacy props`),fe=Symbol(``),pe=Symbol(`proxy path`),me=Symbol(`attributes`),he=Symbol(`class`),ge=Symbol(`style`),_e=Symbol(`text`),ve=Symbol(`form reset`),ye=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},be=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function xe(e){throw Error(`https://svelte.dev/e/experimental_async_required`)}function Se(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function Ce(){throw Error(`https://svelte.dev/e/missing_context`)}function we(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function Te(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function Ee(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function De(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function Oe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function ke(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ae(){throw Error(`https://svelte.dev/e/fork_discarded`)}function je(){throw Error(`https://svelte.dev/e/fork_timing`)}function Me(){throw Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`)}function Ne(){throw Error(`https://svelte.dev/e/hydration_failed`)}function Pe(e){throw Error(`https://svelte.dev/e/lifecycle_legacy_only`)}function Fe(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function Ie(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Le(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Re(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function ze(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}var Be={},w=Symbol(`uninitialized`),Ve=`http://www.w3.org/1999/xhtml`,He=`http://www.w3.org/2000/svg`;function Ue(){console.warn(`https://svelte.dev/e/derived_inert`)}function We(e){console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`)}function Ge(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ke(){console.warn(`https://svelte.dev/e/select_multiple_invalid_value`)}function qe(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var T=!1;function E(e){T=e}var D;function O(e){if(e===null)throw Ge(),Be;return D=e}function k(){return O(R(D))}function Je(e){if(T){if(R(D)!==null)throw Ge(),Be;D=e}}function Ye(e=1){if(T){for(var t=e,n=D;t--;)n=R(n);D=n}}function Xe(e=!0){for(var t=0,n=D;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else(r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=R(n);e&&n.remove(),n=i}}function Ze(e){if(!e||e.nodeType!==8)throw Ge(),Be;return e.data}function Qe(e){return e===this.v}function $e(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function et(e){return!$e(e,this.v)}var tt=!1;function nt(){tt=!0}function rt(e,t){return e.label=t,it(e.v,t),e}function it(e,t){return e?.[pe]?.(t),e}function at(e){return typeof e==`symbol`?`Symbol(${e.description})`:typeof e==`function`?`<function>`:typeof e==`object`&&e?`<object>`:String(e)}var A=null;function ot(e){A=e}function st(){let e={};return[()=>(ut(e)||Ce(),ct(e)),t=>lt(e,t)]}function ct(e){return ht(`getContext`).get(e)}function lt(e,t){return ht(`setContext`).set(e,t),t}function ut(e){return ht(`hasContext`).has(e)}function dt(){return ht(`getAllContexts`)}function ft(e,t=!1,n){A={p:A,i:!1,c:null,e:null,s:e,x:null,r:G,l:tt&&!t?{s:null,u:null,$:[]}:null}}function pt(e){var t=A,n=t.e;if(n!==null){t.e=null;for(var r of n)$n(r)}return e!==void 0&&(t.x=e),t.i=!0,A=t.p,e??{}}function mt(){return!tt||A!==null&&A.l===null}function ht(e){return A===null&&Se(e),A.c??=new Map(gt(A)||void 0)}function gt(e){let t=e.p;for(;t!==null;){let e=t.c;if(e!==null)return e;t=t.p}return null}var _t=[];function vt(){var e=_t;_t=[],h(e)}function j(e){if(_t.length===0&&!rn){var t=_t;queueMicrotask(()=>{t===_t&&vt()})}_t.push(e)}function yt(){for(;_t.length>0;)vt()}function bt(e){var t=G;if(t===null)return H.f|=ue,e;if(!(t.f&32768)&&!(t.f&4))throw e;xt(e,t)}function xt(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);rLine truncated
`).some(e=>e.startsWith(`https://svelte.dev/e/`)))throw r;return r!==Be&&console.warn(`Failed to hydrate: `,r),t.recover===!1&&Ne(),zn(),Un(n),E(!1),yi(e,t)}finally{E(r),O(i)}}var xi=new Map;function Si(e,{target:t,anchor:n,props:r={},events:i,context:o,intro:s=!0,transformError:c}){zn();var l=void 0,u=tr(()=>{var u=n??t.appendChild(I());Rt(u,{pending:()=>{}},t=>{ft({});var n=A;if(o&&(n.c=o),i&&(r.$$events=i),T&&$(t,null),gi=s,l=e(t,r)||{},gi=!0,T&&(G.nodes.end=D,D===null||D.nodeType!==8||D.data!==`]`))throw Ge(),Be;pt()},c);var d=new Set,f=e=>{for(var n=0;n<e.length;n++){var r=e[n];if(!d.has(r)){d.add(r);var i=Jr(r);for(let e of[t,document]){var a=xi.get(e);a===void 0&&(a=new Map,xi.set(e,a));var o=a.get(r);o===void 0?(e.addEventListener(r,oi,{passive:i}),a.set(r,1)):a.set(r,o+1)}}}};return f(a(Qr)),$r.add(f),()=>{for(var e of d)for(let n of[t,document]){var r=xi.get(n),i=r.get(e);--i==0?(n.removeEventListener(e,oi),r.delete(e),r.size===0&&xi.delete(n)):r.set(e,i)}$r.delete(f),u!==n&&u.parentNode?.removeChild(u)}});return Ci.set(l,u),l}var Ci=new WeakMap;function wi(e,t){let n=Ci.get(e);return n?(Ci.delete(e),n(t)):Promise.resolve()}var Ti=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)hr(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(hr(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(V(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();_r(r,t),t.append(I()),this.#n.set(e,{effect:r,fragment:t})}else V(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),pr(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(V(n.effect),this.#n.delete(e))};ensure(e,t){var n=N,r=Wn();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=I();i.append(a),this.#n.set(e,{effect:B(()=>t(a)),fragment:i})}else this.#t.set(e,B(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else T&&(this.anchor=D),this.#a(n)}};function Ei(e,t,n=!1){var r;T&&(r=D,k());var i=new Ti(e),a=n?ne:0;function o(e,t){if(T){var n=Ze(r);if(e!==parseInt(n.substring(1))){var a=Xe();O(a),i.anchor=a,E(!1),i.ensure(e,t),E(!0);return}}i.ensure(e,t)}or(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}var Di=Symbol(`NaN`);function Oi(e,t,n){T&&k();var r=new Ti(e),i=!mt();or(()=>{var e=t();e!==e&&(e=Di),i&&typeof e==`object`&&e&&(e={}),r.ensure(e,n)})}function ki(e,t){return t}function Ai(e,t,n){for(var r=[],i=t.length,o,s=t.length,c=0;c<i;c++){let n=t[c];pr(n,()=>{if(o){if(o.pending.delete(n),o.done.add(n),o.pending.size===0){var t=e.outrogroups;ji(e,a(o.done)),t.delete(o),t.size===0&&(e.outrogroups=null)}}else--s},!1)}if(s===0){var l=r.length===0&&n!==null;if(l){var u=n,d=u.parentNode;Un(d),d.append(u),e.items.clear()}ji(e,t,!l)}else o={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(o)}function ji(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i<t.length;i++){var a=t[i];r?.has(a)?(a.f|=oe,_r(a,document.createDocumentFragment())):V(t[i],n)}}var Mi;function Ni(e,t,r,i,o,s=null){var c=e,l=new Map;if(t&4){var u=e;c=T?O(L(u)):u.appendChild(I())}T&&k();var d=null,f=Jt(()=>{var e=r();return n(e)?e:e==null?[]:a(e)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,Fi(v,p,c,t,i),d!==null&&(p.length===0?d.f&33554432?(d.f^=oe,Li(d,null,c)):hr(d):pr(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:or(()=>{p=Z(f);var e=p.length;let n=!1;T&&Ze(c)===`[!`!=(e===0)&&(c=Xe(),O(c),E(!1),n=!0);for(var a=new Set,u=N,v=Wn(),y=0;y<e;y+=1){T&&D.nodeType===8&&D.data===`]`&&(c=D,n=!0,E(!1));var b=p[y],x=i(b,y),S=h?null:l.get(x);S?(S.v&&Dn(S.v,b),S.i&&Dn(S.i,y),v&&u.unskip_effect(S.e)):(S=Ii(l,h?c:Mi??=I(),b,x,y,o,t,r),h||(S.e.f|=oe),l.set(x,S)),a.add(x)}if(e===0&&s&&!d&&(h?d=B(()=>s(c)):(d=B(()=>s(Mi??=I())),d.f|=oe)),e>a.size&&Te(``,``,``),T&&e>0&&O(Xe()),!h)if(m.set(u,a),v){for(let[e,t]of l)a.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u);n&&E(!0),Z(f)}),flags:t,items:l,pending:m,outrogroups:null,fallback:d};h=!1,T&&(c=D)}function Pi(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function Fi(e,t,n,r,i){var o=!!(r&8),s=t.length,c=e.items,l=Pi(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(o)for(v=0;v<s;v+=1)h=t[v],g=i(h,v),_=c.get(g).e,_.f&33554432||(_.nodes?.a?.measure(),(f??=new Set).add(_));for(v=0;v<s;v+=1){if Line truncated
\r\f\xA0\v`];function oa(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||aa.includes(r[o-1]))&&(s===r.length||aa.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function sa(e,t=!1){var n=t?` !important;`:`;`,r=``;for(var i of Object.keys(e)){var a=e[i];a!=null&&a!==``&&(r+=` `+i+`: `+a+n)}return r}function ca(e){return e[0]!==`-`||e[1]!==`-`?e.toLowerCase():e}function la(e,t){if(t){var n=``,r,i;if(Array.isArray(t)?(r=t[0],i=t[1]):r=t,e){e=String(e).replaceAll(/\s*\/\*.*?\*\/\s*/g,``).trim();var a=!1,o=0,s=!1,c=[];r&&c.push(...Object.keys(r).map(ca)),i&&c.push(...Object.keys(i).map(ca));var l=0,u=-1;let t=e.length;for(var d=0;d<t;d++){var f=e[d];if(s?f===`/`&&e[d-1]===`*`&&(s=!1):a?a===f&&(a=!1):f===`/`&&e[d+1]===`*`?s=!0:f===`"`||f===`'`?a=f:f===`(`?o++:f===`)`&&o--,!s&&a===!1&&o===0){if(f===`:`&&u===-1)u=d;else if(f===`;`||d===t-1){if(u!==-1){var p=ca(e.substring(l,u).trim());if(!c.includes(p)){f!==`;`&&d++;var m=e.substring(l,d).trim();n+=` `+m+`;`}}l=d+1,u=-1}}}}return r&&(n+=sa(r)),i&&(n+=sa(i,!0)),n=n.trim(),n===``?null:n}return e==null?null:String(e)}function ua(e,t,n,r,i,a){var o=e[he];if(T||o!==n||o===void 0){var s=oa(n,r,a);(!T||s!==e.getAttribute(`class`))&&(s==null?e.removeAttribute(`class`):t?e.className=s:e.setAttribute(`class`,s)),e[he]=n}else if(a&&i!==a)for(var c in a){var l=!!a[c];(i==null||l!==!!i[c])&&e.classList.toggle(c,l)}return a}function da(e,t={},n,r){for(var i in n){var a=n[i];t[i]!==a&&(n[i]==null?e.style.removeProperty(i):e.style.setProperty(i,a,r))}}function fa(e,t,n,r){var i=e[ge];if(T||i!==t){var a=la(t,r);(!T||a!==e.getAttribute(`style`))&&(a==null?e.removeAttribute(`style`):e.style.cssText=a),e[ge]=t}else r&&(Array.isArray(r)?(da(e,n?.[0],r[0]),da(e,n?.[1],r[1],`important`)):da(e,n,r));return r}function pa(e,t,r=!1){if(e.multiple){if(t==null)return;if(!n(t))return Ke();for(var i of e.options)i.selected=t.includes(ga(i));return}for(i of e.options)if(Nn(ga(i),t)){i.selected=!0;return}(!r||t!==void 0)&&(e.selectedIndex=-1)}function ma(e){var t=new MutationObserver(()=>{`__value`in e&&pa(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[`value`]}),Zn(()=>{t.disconnect()})}function ha(e,t,n=t){var r=new WeakSet,i=!0;Ft(e,`change`,t=>{var i=t?`[selected]`:`:checked`,a;if(e.multiple)a=[].map.call(e.querySelectorAll(i),ga);else{var o=e.querySelector(i)??e.querySelector(`option:not([disabled])`);a=o&&ga(o)}n(a),e.__value=a,N!==null&&r.add(N)}),nr(()=>{var a=t();if(e===document.activeElement){var o=N;if(r.has(o))return}if(pa(e,a,i),i&&a===void 0){var s=e.querySelector(`:checked`);s!==null&&(a=ga(s),n(a))}e.__value=a,i=!1}),ma(e)}function ga(e){return`__value`in e?e.__value:e.value}var _a=Symbol(`class`),va=Symbol(`style`),ya=Symbol(`is custom element`),ba=Symbol(`is html`),xa=be?`link`:`LINK`,Sa=be?`input`:`INPUT`,Ca=be?`option`:`OPTION`,wa=be?`select`:`SELECT`,Ta=be?`progress`:`PROGRESS`;function Ea(e){if(T){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute(`value`)){var n=e.value;ka(e,`value`,null),e.value=n}if(e.hasAttribute(`checked`)){var r=e.checked;ka(e,`checked`,null),e.checked=r}}};e[ve]=n,j(n),Nt()}}function Da(e,t){var n=Ma(e);n.value!==(n.value=t??void 0)&&(e.value!==t||t===0&&e.nodeName===Ta)&&(e.value=t??``)}function Oa(e,t){t?e.hasAttribute(`selected`)||e.setAttribute(`selected`,``):e.removeAttribute(`selected`)}function ka(e,t,n,r){var i=Ma(e);T&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===xa)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[fe]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&Pa(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Aa(e,t,n,r,i=!1,a=!1){if(T&&i&&e.nodeName===Sa){var o=e;(o.type===`checkbox`?`defaultChecked`:`defaultValue`)in n||Ea(o)}var s=Ma(e),c=s[ya],l=!s[ba];let u=T&&c;u&&E(!1);var d=t||{},f=e.nodeName===Ca;for(var p in t)p in n||(n[p]=null);n.class?n.class=ia(n.class):(r||n[_a])&&(n.class=null),n[va]&&(n.style??=null);var m=Pa(e);if(e.nodeName===Sa&&`type`in n&&(`value`in n||`__value`in n)){var h=n.type;(h!==d.type||h===void 0&&e.hasAttribute(`type`))&&(d.type=h,ka(e,`type`,h,a))}for(let i in n){let o=n[i];if(f&&i===`value`&&o==null){e.value=e.__value=``,d[i]=o;continue}if(i===`class`){ua(e,e.namespaceURI===`http://www.w3.org/1999/xhtml`,o,r,t?.[_a],n[_a]),d[i]=o,d[_a]=n[_a];continue}if(i===`style`){fa(e,o,t?.[va],n[va]),d[i]=o,d[va]=n[va];continue}var g=d[i];if(!(o===g&&!(o===void 0&&e.hasAttribute(i)))){d[i]=o;var _=i[0]+i[1];if(_!==`$$`)if(_===`on`){let t={},n=`$$`+i,r=i.slice(2);var v=Ur(r);if(Vr(r)&&(r=r.slice(0,-7),t.capture=!0),!v&&g){if(o!=null)continue;e.removeEventListener(r,d[n],t),d[n]=null}if(v)ri(r,e,o),ii([r]);else if(o!=null){function a(e){d[i].call(this,e)}d[n]=ti(r,e,a,t)}}else if(i===`style`)ka(e,i,o);else if(i===`autofocus`)At(e,!!o);else if(!c&&(i===`__value`||i===`value`&&o!=null)) Line truncated
@@ -0,0 +1 @@
var e=()=>{document.documentElement.classList.toggle(`dark`,localStorage.theme===`dark`||!(`theme`in localStorage)&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)},t=e=>{e==`light`?localStorage.setItem(`theme`,`light`):e==`dark`?localStorage.setItem(`theme`,`dark`):localStorage.removeItem(`theme`)};export{t as n,e as t};
@@ -1 +0,0 @@
import{C as e,T as t,V as n,W as r,n as i,t as a,w as o,z as s}from"./DxmhE1z1.js";var c=class{constructor(e,t){this.status=e,this.body=typeof t==`string`?{message:t}:t||{message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}},l=class{constructor(e,t){try{new Headers({location:t})}catch{throw Error(`Invalid redirect location ${JSON.stringify(t)}: this string contains characters that cannot be used in HTTP headers`)}this.status=e,this.location=t}},u=class extends Error{constructor(e,t,n){super(n),this.status=e,this.text=t}};new URL(`sveltekit-internal://`);function d(e,t){return e===`/`||t===`ignore`?e:t===`never`?e.endsWith(`/`)?e.slice(0,-1):e:t===`always`&&!e.endsWith(`/`)?e+`/`:e}function f(e){return e.split(`%25`).map(decodeURI).join(`%25`)}function p(e){for(let t in e)e[t]=decodeURIComponent(e[t]);return e}function m({href:e}){return e.split(`#`)[0]}function h(e,t,n,r=!1){let i=new URL(e);Object.defineProperty(i,"searchParams",{value:new Proxy(i.searchParams,{get(e,r){if(r===`get`||r===`getAll`||r===`has`)return(t,...i)=>(n(t),e[r](t,...i));t();let i=Reflect.get(e,r);return typeof i==`function`?i.bind(e):i}}),enumerable:!0,configurable:!0});let a=[`href`,`pathname`,`search`,`toString`,`toJSON`];r&&a.push(`hash`);for(let n of a)Object.defineProperty(i,n,{get(){return t(),e[n]},enumerable:!0,configurable:!0});return i}function g(){}function _(...e){let t=5381;for(let n of e)if(typeof n==`string`){let e=n.length;for(;e;)t=t*33^n.charCodeAt(--e)}else if(ArrayBuffer.isView(n)){let e=new Uint8Array(n.buffer,n.byteOffset,n.byteLength),r=e.length;for(;r;)t=t*33^e[--r]}else throw TypeError(`value must be a string or TypedArray`);return(t>>>0).toString(36)}new TextEncoder;function v(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}var ee=window.fetch;window.fetch=(e,t)=>((e instanceof Request?e.method:t?.method||`GET`)!==`GET`&&y.delete(x(e)),ee(e,t));var y=new Map;function b(e,t){let n=x(e,t),r=document.querySelector(n);if(r?.textContent){r.remove();let{body:e,...t}=JSON.parse(r.textContent);r.getAttribute(`data-b64`)!==null&&(e=v(e));let i=r.getAttribute(`data-ttl`);return i&&y.set(n,{body:e,init:t,ttl:1e3*Number(i)}),Promise.resolve(new Response(e,t))}return window.fetch(e,t)}function te(e,t,n){if(y.size>0){let t=x(e,n),r=y.get(t);if(r){if(performance.now()<r.ttl&&[`default`,`force-cache`,`only-if-cached`,void 0].includes(n?.cache))return new Response(r.body,r.init);y.delete(t)}}return window.fetch(t,n)}function x(e,t){let n=`script[data-sveltekit-fetched][data-url=${JSON.stringify(e instanceof Request?e.url:e)}]`;if(t?.headers||t?.body){let e=[];t.headers&&e.push([...new Headers(t.headers)].join(`,`)),t.body&&(typeof t.body==`string`||ArrayBuffer.isView(t.body))&&e.push(t.body),n+=`[data-hash="${_(...e)}"]`}return n}var ne=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/,re=/^\/\((?:[^)]+)\)$/;function ie(e){let t=[];return{pattern:e===`/`||re.test(e)?/^\/$/:RegExp(`^${oe(e).map(e=>{let n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(e);if(n)return t.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),`(?:/([^]*))?`;let r=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(e);if(r)return t.push({name:r[1],matcher:r[2],optional:!0,rest:!1,chained:!0}),`(?:/([^/]+))?`;if(!e)return;let i=e.split(/\[(.+?)\](?!\])/);return`/`+i.map((e,n)=>{if(n%2){if(e.startsWith(`x+`))return ce(String.fromCharCode(parseInt(e.slice(2),16)));if(e.startsWith(`u+`))return ce(String.fromCharCode(...e.slice(2).split(`-`).map(e=>parseInt(e,16))));let[,r,a,o,s]=ne.exec(e);return t.push({name:o,matcher:s,optional:!!r,rest:!!a,chained:a?n===1&&i[0]===``:!1}),a?`([^]*?)`:r?`([^/]*)?`:`([^/]+?)`}return ce(e)}).join(``)}).join(``)}/?$`),params:t}}function ae(e){return e!==``&&!/^\([^)]+\)$/.test(e)}function oe(e){return e.slice(1).split(`/`).filter(ae)}function se(e,t,n){let r={},i=e.slice(1),a=i.filter(e=>e!==void 0),o=0;for(let e=0;e<t.length;e+=1){let s=t[e],c=i[e-o];if(s.chained&&s.rest&&o&&(c=i.slice(e-o,e+1).filter(e=>e).join(`/`),o=0),c===void 0)if(s.rest)c=``;else continue;if(!s.matcher||n[s.matcher](c)){r[s.name]=c;let n=t[e+1],l=i[e+1];n&&!n.rest&&n.optional&&l&&s.chained&&(o=0),!n&&!l&&Object.keys(r).length===a.length&&(o=0);continue}if(s.optional&&s.chained){o++;continue}return}if(!o)return r}function ce(e){return e.normalize().replace(/[[\]]/g,`\\$&`).replace(/%/g,`%25`).replace(/\//g,`%2[Ff]`).replace(/\?/g,`%3[Ff]`).replace(/#/g,`%23`).replace(/[.*+?^${}()|\\]/g,`\\$&`)}function le({nodes:e,server_loads:t,dictionary:n,matchers:r}){let i=new Set(t);return Object.entries(n).map(([t,[n,i,s]])=>{let{pattern:c,params:l}=ie(t),u={id:t,exec:e=>{let t=c.exec(e);if(t)return se(t,l,r)},errors:[1,...s||[]].map(t=>e[t]),layouts:[0,...i||[]].map(o),leaf:a(n)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function a(t){let n=t<0;return n&&(t=~t),[n,e[t]]}function o(t){return t===void 0?t:[i.has(t),e[t]]}}function ue(e,t=JSON.parse){try{return t(sessionStorage[e] Line truncated
@@ -0,0 +1,6 @@
import{$ as e,A as t,B as n,D as r,E as i,F as a,H as o,I as s,K as c,L as l,M as u,N as d,P as f,Q as p,R as m,T as h,U as g,X as _,Z as v,_ as y,a as b,at as x,b as ee,c as te,dt as ne,et as S,f as re,ft as ie,h as ae,it as oe,j as C,k as w,m as se,n as ce,nt as T,p as le,q as E,r as ue,u as de,v as fe,vt as D}from"./DGrvnoUE.js";import"./BZuNOv8h.js";import"./xihTtKlq.js";import"./B6JECYQD.js";var pe=Math.min,O=Math.max,me=Math.round,he=Math.floor,k=e=>({x:e,y:e}),A={left:`right`,right:`left`,bottom:`top`,top:`bottom`};function j(e,t,n){return O(e,pe(t,n))}function ge(e,t){return typeof e==`function`?e(t):e}function M(e){return e.split(`-`)[0]}function N(e){return e.split(`-`)[1]}function P(e){return e===`x`?`y`:`x`}function F(e){return e===`y`?`height`:`width`}function I(e){let t=e[0];return t===`t`||t===`b`?`y`:`x`}function _e(e){return P(I(e))}function ve(e,t,n){n===void 0&&(n=!1);let r=N(e),i=_e(e),a=F(i),o=i===`x`?r===(n?`end`:`start`)?`right`:`left`:r===`start`?`bottom`:`top`;return t.reference[a]>t.floating[a]&&(o=Ee(o)),[o,Ee(o)]}function ye(e){let t=Ee(e);return[be(e),t,be(t)]}function be(e){return e.includes(`start`)?e.replace(`start`,`end`):e.replace(`end`,`start`)}var xe=[`left`,`right`],Se=[`right`,`left`],Ce=[`top`,`bottom`],L=[`bottom`,`top`];function we(e,t,n){switch(e){case`top`:case`bottom`:return n?t?Se:xe:t?xe:Se;case`left`:case`right`:return t?Ce:L;default:return[]}}function Te(e,t,n,r){let i=N(e),a=we(M(e),n===`start`,r);return i&&(a=a.map(e=>e+`-`+i),t&&(a=a.concat(a.map(be)))),a}function Ee(e){let t=M(e);return A[t]+e.slice(t.length)}function De(e){return{top:e.top??0,right:e.right??0,bottom:e.bottom??0,left:e.left??0}}function Oe(e){return typeof e==`number`?{top:e,right:e,bottom:e,left:e}:De(e)}function ke(e){let{x:t,y:n,width:r,height:i}=e;return{width:r,height:i,top:n,left:t,right:t+r,bottom:n+i,x:t,y:n}}function Ae(e,t,n){let{reference:r,floating:i}=e,a=I(t),o=_e(t),s=F(o),c=M(t),l=a===`y`,u=r.x+r.width/2-i.width/2,d=r.y+r.height/2-i.height/2,f=r[s]/2-i[s]/2,p;switch(c){case`top`:p={x:u,y:r.y-i.height};break;case`bottom`:p={x:u,y:r.y+r.height};break;case`right`:p={x:r.x+r.width,y:d};break;case`left`:p={x:r.x-i.width,y:d};break;default:p={x:r.x,y:r.y}}let m=N(t);return m&&(p[o]+=f*(m===`end`?1:-1)*(n&&l?-1:1)),p}async function je(e,t){t===void 0&&(t={});let{x:n,y:r,platform:i,rects:a,elements:o,strategy:s}=e,{boundary:c=`clippingAncestors`,rootBoundary:l=`viewport`,elementContext:u=`floating`,altBoundary:d=!1,padding:f=0}=ge(t,e),p=Oe(f),m=o[d?u===`floating`?`reference`:`floating`:u],h=ke(await i.getClippingRect({element:await(i.isElement==null?void 0:i.isElement(m))??!0?m:m.contextElement||await(i.getDocumentElement==null?void 0:i.getDocumentElement(o.floating)),boundary:c,rootBoundary:l,strategy:s})),g=u===`floating`?{x:n,y:r,width:a.floating.width,height:a.floating.height}:a.reference,_=await(i.getOffsetParent==null?void 0:i.getOffsetParent(o.floating)),v=await(i.isElement==null?void 0:i.isElement(_))&&await(i.getScale==null?void 0:i.getScale(_))||{x:1,y:1},y=ke(i.convertOffsetParentRelativeRectToViewportRelativeRect?await i.convertOffsetParentRelativeRectToViewportRelativeRect({elements:o,rect:g,offsetParent:_,strategy:s}):g);return{top:(h.top-y.top+p.top)/v.y,bottom:(y.bottom-h.bottom+p.bottom)/v.y,left:(h.left-y.left+p.left)/v.x,right:(y.right-h.right+p.right)/v.x}}var Me=50,Ne=async(e,t,n)=>{let{placement:r=`bottom`,strategy:i=`absolute`,middleware:a=[],platform:o}=n,s=o.detectOverflow?o:{...o,detectOverflow:je},c=await(o.isRTL==null?void 0:o.isRTL(t)),l=await o.getElementRects({reference:e,floating:t,strategy:i}),{x:u,y:d}=Ae(l,r,c),f=r,p=0,m={};for(let n=0;n<a.length;n++){let h=a[n];if(!h)continue;let{name:g,fn:_}=h,{x:v,y,data:b,reset:x}=await _({x:u,y:d,initialPlacement:r,placement:f,strategy:i,middlewareData:m,rects:l,platform:s,elements:{reference:e,floating:t}});u=v??u,d=y??d,m[g]={...m[g],...b},x&&p<Me&&(p++,typeof x==`object`&&(x.placement&&(f=x.placement),x.rects&&(l=x.rects===!0?await o.getElementRects({reference:e,floating:t,strategy:i}):x.rects),{x:u,y:d}=Ae(l,f,c)),n=-1)}return{x:u,y:d,placement:f,strategy:i,middlewareData:m}},Pe=function(e){return e===void 0&&(e={}),{name:`flip`,options:e,async fn(t){var n;let{placement:r,middlewareData:i,rects:a,initialPlacement:o,platform:s,elements:c}=t,{mainAxis:l=!0,crossAxis:u=!0,fallbackPlacements:d,fallbackStrategy:f=`bestFit`,fallbackAxisSideDirection:p=`none`,flipAlignment:m=!0,...h}=ge(e,t);if((n=i.arrow)!=null&&n.alignmentOffset)return{};let g=M(r),_=I(o),v=M(o)===o,y=await(s.isRTL==null?void 0:s.isRTL(c.floating)),b=d||(v||!m?[Ee(o)]:ye(o)),x=p!==`none`;!d&&x&&b.push(...Te(o,m,p,y));let ee=[o,...b],te=await s.detectOverflow(t,h),ne=[],S=i.flip?.overflows||[];if(l&&ne.push(te[g]),u){let e=ve(r,a,y);ne.push(te[e[0]],te[e[1]])}if(S=[...S,{placement:r,overflows:ne}],!ne.every(e=>e<=0)){let e=(i.flip?.index||0)+1,t=ee[e];if(t&&(u!==`alignment`||_===I(t)||S.every(e=>I(e.placement)!==_||e.overflows[0]>0)))return{daLine truncated
3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0
1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502
0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0
0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"></path></svg>`);function Nt(e){var t=Mt();C(e,t)}var Q=f(`<svg width="100%" height="100%" viewBox="-2 -2 50 50" focusable="false" aria-hidden="true" role="presentation"><path fill="currentColor" d="M34.923,37.251L24,26.328L13.077,37.251L9.436,33.61l10.923-10.923L9.436,11.765l3.641-3.641L24,19.047L34.923,8.124
l3.641,3.641L27.641,22.688L38.564,33.61L34.923,37.251z"></path></svg>`);function Pt(e){var t=Q();C(e,t)}var Ft=f(`<svg class="loading" viewBox="25 25 50 50"><circle class="circle_path" cx="50" cy="50" r="20" fill="none" stroke="currentColor" stroke-width="5" stroke-miterlimit="10"></circle></svg>`);function It(e){var t=Ft();C(e,t)}var Lt=d(`<div class="list-item" tabindex="-1" role="none"><div><!></div></div>`),Rt=d(`<div class="empty">No options</div>`),zt=d(`<div role="none"><!> <!> <!></div>`),Bt=d(`<span id="aria-selection"> </span> <span id="aria-context"> </span>`,1),Vt=d(`<div class="multi-item-clear"><!></div>`),Ht=d(`<div role="none"><span class="multi-item-text"><!></span> <!></div>`),Ut=d(`<div><!></div>`),Wt=d(`<div class="icon loading" aria-hidden="true"><!></div>`),Gt=d(`<button type="button" class="icon clear-select"><!></button>`),Kt=d(`<div class="icon chevron" aria-hidden="true"><!></div>`),qt=d(`<input type="hidden"/>`),Jt=d(`<select class="required" required="" tabindex="-1" aria-hidden="true"></select>`),Yt=d(`<div role="none"><!> <span aria-live="polite" aria-atomic="false" aria-relevant="additions text" class="a11y-text"><!></span> <div class="prepend"><!></div> <div class="value-container"><!> <input/></div> <div class="indicators"><!> <!> <!></div> <!> <!></div>`);function Xt(o,s){ie(s,!0);let d,f=b(s,`valueMode`,3,`item`),pe=b(s,`filter`,3,At),O=b(s,`getItems`,3,jt),me=b(s,`id`,3,null),he=b(s,`name`,3,null),k=b(s,`container`,15,null),A=b(s,`input`,15,null),j=b(s,`multiple`,3,!1),ge=b(s,`multiFullItemClearable`,3,!1),M=b(s,`disabled`,3,!1),N=b(s,`focused`,15,!1),P=b(s,`value`,15),F=b(s,`filterText`,15,``),I=b(s,`placeholder`,3,`Please select`),_e=b(s,`placeholderAlwaysShow`,3,!1),ve=b(s,`items`,31,()=>S([])),ye=b(s,`label`,3,`label`),be=b(s,`itemFilter`,3,(e,t,n)=>`${e}`.toLowerCase().includes(t.toLowerCase())),xe=b(s,`groupBy`,3,void 0),Se=b(s,`groupFilter`,3,e=>e),Ce=b(s,`groupHeaderSelectable`,3,!1),L=b(s,`itemId`,3,`value`),we=b(s,`loadOptions`,3,void 0),Te=b(s,`containerStyles`,3,``),Ee=b(s,`hasError`,3,!1),De=b(s,`filterSelectedItems`,3,!0),Oe=b(s,`required`,3,!1),ke=b(s,`closeListOnChange`,3,!0),Ae=b(s,`clearFilterTextOnBlur`,3,!0),je=b(s,`createGroupHeaderItem`,3,(e,t)=>({value:e,[ye()]:e})),Me=b(s,`searchable`,3,!0),Ne=b(s,`inputStyles`,3,``),Pe=b(s,`clearable`,3,!0),Fe=b(s,`loading`,15,!1),R=b(s,`listOpen`,15,!1),Ie=b(s,`debounce`,3,(e,t=1)=>{clearTimeout(d),d=setTimeout(e,t)}),Le=b(s,`debounceWait`,3,300),Re=b(s,`hideEmptyState`,3,!1),z=b(s,`inputAttributes`,19,()=>({})),B=b(s,`listAutoWidth`,3,!0),V=b(s,`showChevron`,3,!1),ze=b(s,`listOffset`,3,5),H=b(s,`hoverItemIndex`,15,0),U=b(s,`floatingConfig`,19,()=>({})),Be=b(s,`class`,3,``),Ve=b(s,`ariaValues`,3,e=>`Option ${e}, selected.`),He=b(s,`ariaListOpen`,3,(e,t)=>`You are currently focused on option ${e}. There are ${t} results available.`),Ue=b(s,`ariaFocused`,3,()=>`Select is focused, type to refine list, press down to open the menu.`);function We(){return n(Z)}let W=oe(void 0),G,Ge,Ke,qe,K=oe(null),q={},J=x(()=>{if(z()||!Me()){let e=Object.assign({autocapitalize:`none`,autocomplete:`off`,autocorrect:`off`,spellcheck:!1,tabindex:0,type:`text`,"aria-autocomplete":`list`},z());me()&&(e.id=me()),Me()||(e.readonly=!0),JSON.stringify(e)!==JSON.stringify(q)&&(q=e)}return q});function Y(e){return e==null?e:typeof e==`object`?e[L()]:e}function Je(e){return Array.isArray(e)&&e.length>0&&typeof e[0]!=`object`}let Ye=x(()=>f()===`id`||Je(ve()));function Xe(e){return n(Ye)?Y(e):e}function Ze(e){if(e==null)return e;if(typeof e==`object`)return e[ye()];let t=ve()?.find(t=>t[L()]===e);return t?t[ye()]:e}function Qe(){return P()==null?null:n(Ye)?j()&&Array.isArray(P())?JSON.stringify(P()):`${P()}`:JSON.stringify(P())}function $e(e){return e.map((e,t)=>({index:t,value:e,label:`${e}`}))}function et(e){if(!xe())return e;let t=[],n={};e.forEach(e=>{let r=xe()(e)??``;t.includes(r)||(t.push(r),n[r]=[],r&&n[r].push(Object.assign(je()(r,e),{id:r,groupHeader:!0,selectable:Ce()}))),n[r].push(Object.assign({groupItem:!!r},e))});let r=[];return Se()(t).forEach(e=>{n[e]&&r.push(...n[e])}),r}function tt(){if(j()){JSON.stringify(P())!==JSON.stringify(G)&&(!P()||_t())&&s.oninput?.(P());return}P()?(!G||JSON.stringify(Y(P()))!==JSON.stringify(Y(G)))&&s.oninput?.(P()):G&&s.oninput?.(P())}function nt(){Ke=!0,P()&&(Array.isArray(P())||P([P()]))}function rt(){Ke=!1,P()&&P(void 0)}function X(){it(n(Z).findIndex(e=>e[L()]===Y(P())),!0)}function it(e=0,t){let r=e<0?0:e;H()!==r&&H(r),!t&&xe()&&n(Z)[H()]&&!n(Z)[H()].selectable&&tn(1)}function at(){!we()&&F().length===0||(we()?Ie()(async function(){Fe(!0);let e=await O()({onerror:s.onerror,onloaded:s.onloaded,loadOptions:we(),convertStringItemsToObjects:$e,filterText:F()});e?(Fe(e.loading),R(R()?e.listOpen:F().length>0),N(R()&&e.focused),ve(xe()?et(e.filteredItems):e.filteredItems)):(Fe(!1),N(!0),R(!0))},Le()):(R()||R(!0),j()&&T(W,void 0)))}let ot=x(()=>j()?Array.isArray(P())&&P().length>0:!!P()),st=x(()=>n(Line truncated
@@ -0,0 +1,2 @@
import{et as e}from"./DGrvnoUE.js";import{t}from"./BZuNOv8h.js";import"./CY9OlB4q.js";var n=e({user:null,status:`loading`}),r=()=>{localStorage.removeItem(`accessToken`),n.status=`unauthenticated`},i=()=>localStorage.getItem(`accessToken`),a=e=>{localStorage.setItem(`accessToken`,e)},o=`/api`,s=class extends Error{status;body;constructor(e,t){super(`API Error ${e}`),this.status=e,this.body=t}};async function c(e,n={}){let{auth:a=!0,headers:c,...l}=n,d={"Content-Type":`application/json`,...c},f=new Headers(d);if(a){let e=i();e&&f.append(`Authorization`,`Bearer ${e}`)}let p=await fetch(`${o}${e}`,{...l,headers:f});if(p.status===401&&(r(),t(`/login`)),!p.ok)throw new s(p.status,await u(p));if(p.status!==204)return p.json()}async function*l(e,n={}){let{auth:a=!0,headers:s,...c}=n,l={"Content-Type":`application/json`,...s},u=new Headers(l);if(a){let e=i();e&&u.append(`Authorization`,`Bearer ${e}`)}let d=await fetch(`${o}${e}`,{...c,headers:u});if(d.status===401&&(r(),t(`/login`)),!d.ok||!d.body)throw Error(`HTTP ${d.status}`);let f=d.body.pipeThrough(new TextDecoderStream).getReader(),p=``;try{for(;;){let{value:e,done:t}=await f.read();if(t)break;p+=e;let n=p.split(`
`);p=n.pop()??``;for(let e of n)e.trim()&&(yield JSON.parse(e))}p.trim()&&(yield JSON.parse(p))}finally{f.releaseLock()}}async function u(e){try{return await e.json()}catch{return null}}var d={get:(e,t)=>c(e,{...t,method:`GET`}),post:(e,t,n)=>c(e,{...n,method:`POST`,body:JSON.stringify(t)}),postStream:(e,t,n)=>l(e,{...n,method:`POST`,body:JSON.stringify(t)}),put:(e,t,n)=>c(e,{...n,method:`PUT`,body:JSON.stringify(t)}),delete:(e,t)=>c(e,{...t,method:`DELETE`})};export{a as i,n,r,d as t};
@@ -1,3 +0,0 @@
var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r},n=Array.isArray,r=Array.prototype.indexOf,i=Array.prototype.includes,a=Array.from,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyDescriptors,l=Object.prototype,u=Array.prototype,d=Object.getPrototypeOf,f=Object.isExtensible,p=()=>{};function m(e){for(var t=0;t<e.length;t++)e[t]()}function h(){var e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}var g=1024,_=2048,v=4096,y=8192,ee=16384,te=32768,ne=1<<25,re=65536,ie=1<<18,ae=1<<19,oe=1<<20,se=65536,ce=1<<21,le=1<<22,ue=1<<23,de=Symbol(`$state`),fe=Symbol(`legacy props`),pe=Symbol(``),me=Symbol(`proxy path`),he=Symbol(`attributes`),ge=Symbol(`class`),_e=Symbol(`style`),ve=Symbol(`text`),ye=Symbol(`form reset`),be=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},xe=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function Se(e){throw Error(`https://svelte.dev/e/experimental_async_required`)}function Ce(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function we(){throw Error(`https://svelte.dev/e/missing_context`)}function Te(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function Ee(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function De(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function Oe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function ke(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ae(){throw Error(`https://svelte.dev/e/fork_discarded`)}function je(){throw Error(`https://svelte.dev/e/fork_timing`)}function Me(){throw Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`)}function Ne(){throw Error(`https://svelte.dev/e/hydration_failed`)}function Pe(e){throw Error(`https://svelte.dev/e/lifecycle_legacy_only`)}function Fe(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function Ie(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Le(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Re(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function ze(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}var Be={},b=Symbol(`uninitialized`),Ve=`http://www.w3.org/1999/xhtml`;function He(){console.warn(`https://svelte.dev/e/derived_inert`)}function Ue(e){console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`)}function We(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ge(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var x=!1;function S(e){x=e}var C;function w(e){if(e===null)throw We(),Be;return C=e}function Ke(){return w(I(C))}function qe(e){if(x){if(I(C)!==null)throw We(),Be;C=e}}function Je(e=1){if(x){for(var t=e,n=C;t--;)n=I(n);C=n}}function Ye(e=!0){for(var t=0,n=C;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else(r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=I(n);e&&n.remove(),n=i}}function Xe(e){if(!e||e.nodeType!==8)throw We(),Be;return e.data}function Ze(e){return e===this.v}function Qe(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function $e(e){return!Qe(e,this.v)}function et(e,t){return e.label=t,tt(e.v,t),e}function tt(e,t){return e?.[me]?.(t),e}function nt(e){return typeof e==`symbol`?`Symbol(${e.description})`:typeof e==`function`?`<function>`:typeof e==`object`&&e?`<object>`:String(e)}var T=null;function rt(e){T=e}function it(){let e={};return[()=>(st(e)||we(),at(e)),t=>ot(e,t)]}function at(e){return ft(`getContext`).get(e)}function ot(e,t){return ft(`setContext`).set(e,t),t}function st(e){return ft(`hasContext`).has(e)}function ct(){return ft(`getAllContexts`)}function lt(e,t=!1,n){T={p:T,i:!1,c:null,e:null,s:e,x:null,r:W,l:null}}function ut(e){var t=T,n=t.e;if(n!==null){t.e=null;for(var r of n)Un(r)}return e!==void 0&&(t.x=e),t.i=!0,T=t.p,e??{}}function dt(){return!0}function ft(e){return T===null&&Ce(e),T.c??=new Map(pt(T)||void 0)}function pt(e){let t=e.p;for(;t!==null;){let e=t.c;if(e!==null)return e;t=t.p}return null}var mt=[];function ht(){var e=mt;mt=[],m(e)}function E(e){if(mt.length===0&&!Qt){var t=mt;queueMicrotask(()=>{t===mt&&ht()})}mt.push(e)}function gt(){for(;mt.length>0;)ht()}function _t(e){var t=W;if(t===null)return V.f|=ue,e;if(!(t.f&32768)&&!(t.f&4))throw e;D(e,t)}function D(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}var vt=~(_|v|g);function O(e,t){e.f=e.f&vt|t}function yt(e){e.f&512||e.deps===null?O(e,g):O(e,v)}function bt(e){if(e!==null)for(let t of e)!(t.f&2)||!(t.f&65536)||(t.f^=se,bt(t.deps))}function xt(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),bt(e.deps),O(e,g)}var St=[];function Ct(e,t=p){letLine truncated
`).some(e=>e.startsWith(`https://svelte.dev/e/`)))throw r;return r!==Be&&console.warn(`Failed to hydrate: `,r),t.recover===!1&&Ne(),An(),Pn(n),S(!1),Gr(e,t)}finally{S(r),w(i)}}var qr=new Map;function Jr(e,{target:t,anchor:n,props:r={},events:i,context:o,intro:s=!0,transformError:c}){An();var l=void 0,u=Gn(()=>{var s=n??t.appendChild(P());Nt(s,{pending:()=>{}},t=>{lt({});var n=T;if(o&&(n.c=o),i&&(r.$$events=i),x&&$(t,null),l=e(t,r)||{},x&&(W.nodes.end=C,C===null||C.nodeType!==8||C.data!==`]`))throw We(),Be;ut()},c);var u=new Set,d=e=>{for(var n=0;n<e.length;n++){var r=e[n];if(!u.has(r)){u.add(r);var i=Dr(r);for(let e of[t,document]){var a=qr.get(e);a===void 0&&(a=new Map,qr.set(e,a));var o=a.get(r);o===void 0?(e.addEventListener(r,Ir,{passive:i}),a.set(r,1)):a.set(r,o+1)}}}};return d(a(kr)),Ar.add(d),()=>{for(var e of u)for(let n of[t,document]){var r=qr.get(n),i=r.get(e);--i==0?(n.removeEventListener(e,Ir),r.delete(e),r.size===0&&qr.delete(n)):r.set(e,i)}Ar.delete(d),s!==n&&s.parentNode?.removeChild(s)}});return Yr.set(l,u),l}var Yr=new WeakMap;function Xr(e,t){let n=Yr.get(e);return n?(Yr.delete(e),n(t)):Promise.resolve()}var Zr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)ir(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(ir(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(z(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();or(r,t),t.append(P()),this.#n.set(e,{effect:r,fragment:t})}else z(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),nr(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(z(n.effect),this.#n.delete(e))};ensure(e,t){var n=k,r=Fn();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=P();i.append(a),this.#n.set(e,{effect:R(()=>t(a)),fragment:i})}else this.#t.set(e,R(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else x&&(this.anchor=C),this.#a(n)}};function Qr(e,t,n=!1){var r;x&&(r=C,Ke());var i=new Zr(e),a=n?re:0;function o(e,t){if(x){var n=Xe(r);if(e!==parseInt(n.substring(1))){var a=Ye();w(a),i.anchor=a,S(!1),i.ensure(e,t),S(!0);return}}i.ensure(e,t)}Xn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}function $r(e,t,...n){var r=new Zr(e);Xn(()=>{let e=t()??null;r.ensure(e,e&&(t=>e(t,...n)))},re)}function ei(e){return(t,...n)=>{var r=e(...n),i;x?(i=C,Ke()):(i=F(zr(r.render().trim())),t.before(i));let a=r.setup?.(i);$(i,i),typeof a==`function`&&Vn(a)}}function ti(e,t,n){var r;x&&(r=C,Ke());var i=new Zr(e);Xn(()=>{var e=t()??null;if(x&&Xe(r)===`[`!=(e!==null)){var a=Ye();w(a),i.anchor=a,S(!1),i.ensure(e,e&&(t=>n(t,e))),S(!0);return}i.ensure(e,e&&(t=>n(t,e)))},re)}function ni(e,t){let n=null,r=x;var i;if(x){n=C;for(var a=F(document.head);a!==null&&(a.nodeType!==8||a.data!==e);)a=I(a);if(a===null)S(!1);else{var o=I(a);a.remove(),w(o)}}x||(i=document.head.appendChild(P()));try{Xn(()=>{var e=R(()=>t(i));e.f|=ie})}finally{r&&(S(!0),w(n))}}function ri(e){var t,n,r=``;if(typeof e==`string`||typeof e==`number`)r+=e;else if(typeof e==`object`)if(Array.isArray(e)){var i=e.length;for(t=0;t<i;t++)e[t]&&(n=ri(e[t]))&&(r&&(r+=` `),r+=n)}else for(n in e)e[n]&&(r&&(r+=` `),r+=n);return r}function ii(){for(var e,t,n=0,r=``,i=arguments.length;n<i;n++)(e=arguments[n])&&(t=ri(e))&&(r&&(r+=` `),r+=t);return r}function ai(e){return typeof e==`object`?ii(e):e??``}var oi=[...`
\r\f\xA0\v`];function si(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||oi.includes(r[o-1]))&&(s===r.length||oi.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function ci(e,t,n,r,i,a){var o=e[ge];if(x||o!==n||o===void 0){var s=si(n,r,a);(!x||s!==e.getAttribute(`class`))&&(s==null?e.removeAttribute(`class`):t?e.className=s:e.setAttribute(`class`,s)),e[ge]=n}else if(a&&i!==a)for(var c in a){var l=!!a[c];(i==null||l!==!!i[c])&&e.classList.toggle(c,l)}return a}var li=Symbol(`is custom element`),ui=Symbol(`is html`),di=xe?`link`:`LINK`;function fi(e){if(x){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute(`value`)){var n=e.value;pi(e,`value`,null),e.value=n}if(e.hasAttribute(`checked`)){var r=e.checked;pi(e,`checked`,null),e.checked=r}}};e[ye]=n,E(n),Ot()}}function pi(e,t,n,r){var i=mi(e);x&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===di)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[pe]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&gi(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function mi(e){return e[he]??={[li]:e.nodeName.includes(`-`),[ui]:e.namespaceURI===Ve}}var hi=new Map;function gi(e){var t=e.getAttribute(`is`)||e.nodeName,n=hi.get(t);if(n)return n;hi.set(t,n=[]);for(var r,i=e,a=Element.prototype;a!==i;){for(var o in r=c(i),r)r[o].set&&o!==`innerHTML`&&o!==`textContent`&&o!==`innerText`&&n.push(o);i=d(i)}return n}function _i(e,t,n=t){var r=new WeakSet;At(e,`input`,async i=>{var a=i?e.defaultValue:e.value;if(a=vi(e)?yi(a):a,n(a),k!==null&&r.add(k),await Sr(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(x&&e.defaultValue!==e.value||Q(t)==null&&e.value)&&(n(vi(e)?yi(e.value):e.value),k!==null&&r.add(k)),Jn(()=>{var n=t();if(e===document.activeElement){var i=k;if(r.has(i))return}vi(e)&&n===yi(e.value)||e.type===`date`&&!n&&!e.value||n!==e.value&&(e.value=n??``)})}function vi(e){var t=e.type;return t===`number`||t===`range`}function yi(e){return e===``?null:+e}function bi(e,t){return e===t||e?.[de]===t}function xi(e={},t,n,r){var i=T.r,a=W;return Kn(()=>{var o,s;return Jn(()=>{o=s,s=r?.()||[],Q(()=>{bi(n(...s),e)||(t(e,...s),o&&bi(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&bi(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function Si(e,t,n,r){var i=!0,a=!!(n&8),o=!!(n&16),c=r,l=!0,u=void 0,d=()=>o&&i?(u??=zt(r),Z(u)):(l&&(l=!1,c=o?Q(r):r),c);let f;if(a){var p=de in e||fe in e;f=s(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;a?[m,h]=Tt(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Fe(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(c=void 0),n===void 0?c:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?zt:Ut)(()=>(v=!1,g()));a&&Z(y);var ee=W;return(function(e,t){if(arguments.length>0){let n=t?Z(y):i&&a?wn(e):e;return N(y,n),v=!0,c!==void 0&&(c=n),e}return B&&v||ee.f&16384?y.v:Z(y)})}function Ci(e){return class extends wi{constructor(t){super({component:e,...t})}}}var wi=class{#e;#t;constructor(e){var t=new Map,n=(e,n)=>{var r=yn(n,!1,!1);return t.set(e,r),r};let r=new Proxy({...e.props||{},$$events:{}},{get(e,r){return Z(t.get(r)??n(r,Reflect.get(e,r)))},has(e,r){return r===fe||(Z(t.get(r)??n(r,Reflect.get(e,r))),Reflect.has(e,r))},set(e,r,i){return N(t.get(r)??n(r,i),i),Reflect.set(e,r,i)}});this.#t=(e.hydrate?Kr:Gr)(e.component,{target:e.target,anchor:e.anchor,props:r,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError}),(!e?.props?.$$host||e.sync===!1)&&on(),this.#e=r.$$events;for(let e of Object.keys(this.#t))e!==`$set`&&e!==`$destroy`&&e!==`$on`&&o(this,e,{get(){return this.#t[e]},set(t){this.#t[e]=t},enumerable:!0});this.#t.$set=e=>{Object.assign(r,e)},this.#t.$destroy=()=>{Xr(this.#t)}}$set(e){this.#t.$set(e)}$on(e,t){this.#e[e]=this.#e[e]||[];let n=(...e)=>t.call(this,...e);return this.#e[e].push(n),()=>{this.#e[e]=this.#e[e].filter(e=>e!==n)}}$destroy(){this.#t.$destroy()}};function Ti(e,t){if(Se(`hydratable`),x){let t=window.__svelte?.h;if(t?.has(e))return t.get(e);Ue(e)}return t()}var Ei=t({afterUpdate:()=>Ni,beforeUpdate:()=>Mi,createContext:()=>it,createEventDispatcher:()=>ji,createRawSnippet:()=>ei,flushSync:()=>on,fork:()=>pn,getAbortSignal:()=>Di,getAllContexts:()=>ct,getContext:()=>at,hasContext:()=>st,hydratable:()=>Ti,hydrate:()=>Kr,mount:()=>Gr,onDestroy:()=>ki,onMount:()=>Oi,setContext:()=>ot,settled:()=>Cr,tick:()=>Sr,unmount:()=>Xr,untrack:()=>Q});function Di(){return V===null&&Me(),(V.ac??=new AbortControlle Line truncated
@@ -1 +0,0 @@
import{n as e}from"./puscbPt7.js";var t={register:t=>e.post(`/auth/register`,t),status:()=>e.get(`/auth/status`),login:t=>e.post(`/auth/login`,t)};export{t};
@@ -0,0 +1 @@
import"./DGrvnoUE.js";import{i as e,o as t}from"./BZuNOv8h.js";var n={get data(){return t.data},get error(){return t.error},get form(){return t.form},get params(){return t.params},get route(){return t.route},get state(){return t.state},get status(){return t.status},get url(){return t.url}};e.updated.check;var r=n;export{r as t};
@@ -1 +0,0 @@
import{L as e}from"./DxmhE1z1.js";import{t}from"./DZTG_XPZ.js";var n=e({user:null,status:`loading`}),r=()=>{localStorage.removeItem(`accessToken`),n.status=`unauthenticated`},i=()=>localStorage.getItem(`accessToken`),a=e=>{localStorage.setItem(`accessToken`,e)},o=`/api`,s=class extends Error{status;body;constructor(e,t){super(`API Error ${e}`),this.status=e,this.body=t}};async function c(e,n={}){let{auth:a=!0,headers:c,...u}=n,d={"Content-Type":`application/json`,...c},f=new Headers(d);if(a){let e=i();e&&f.append(`Authorization`,`Bearer ${e}`)}let p=await fetch(`${o}${e}`,{...u,headers:f});if(p.status===401&&(r(),t(`/login`)),!p.ok)throw new s(p.status,await l(p));if(p.status!==204)return p.json()}async function l(e){try{return await e.json()}catch{return null}}var u={get:(e,t)=>c(e,{...t,method:`GET`}),post:(e,t,n)=>c(e,{...n,method:`POST`,body:JSON.stringify(t)}),put:(e,t,n)=>c(e,{...n,method:`PUT`,body:JSON.stringify(t)}),delete:(e,t)=>c(e,{...t,method:`DELETE`})},d=`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXoAAAA1CAYAAABP2SCdAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAClxJREFUeJzt3W2MXFUZB/D/c+6d7WK7c6e73btbWgSUlshrlQ8YAoKQIIkhBk2lO01rEz6YUrTxJTRqsKyaygfxLUgUTKi4c7dQEU2IiIIaRAOUtxZTlZgUQdqdu9vuzvR9Z+Y8fmi7vXdmdu/c05md5fL8PvWePec8596982x67pxziZkhhBAiuVS7ByCEEKK17FP/WDg0ch2T6pipMpOeKGT7X2z9sIRozPxtY2enKvqSYBlZ+o3xVf1vtmlIQsw5U4leK/UYgO6Zq6sXAVzZ2iEJ0ThL841MeChUyNbXANzTnhEJMffI1I0QQiScJHohhEg4SfRCCJFwU3P0RPg5M+ZPHYM6GXxbe4YlWiE9nL+ZWJ0bLFOTqaHxdZmJdo1JCNF6U4l+YsDdFPxB1/aRXlVSkugThDTdDuKbgmWVeZWnAUiiFyLBZOpGCCESThK9EEIknCR6IYRIODu6ikgKTXiQgGeCZWQf9ds1HiHE7JBE/x5yMOv+ut1jEELMPpm6EUKIhJNEL4QQCSeJXgghEq6tc/SO598H0Pvjt+RXCln3bqOYudHvgHBZ/JZ6TyHbt9EkZtrzv0qgj8VvyeOFrPs5s5j52wjqU1H1SmW1/sjanndMYlTLDPufYSaj8arJ1NqoFbrpR8aWU4W/Fywj8DnV9VhjreONXhUZU1W+aLKdcWZo7COs+O647U7ExObxVb2vzlSHtu/uSJd6f2XSf92YqPxgPNv/57jtMl5hIWPyFyYxNXiryTMhAijtjf7WJGbd/pjvm1jt/iFYlh7ObyFWoa2trZS95sDKhYW4/XcNj12rmL8SikkYmhjofTSqrZPL/xikzosbE8DOQrb3rjgN2vswlul6EH/IoOU885C4isAfj90OtMs0JoGuAPhmg6Z545ikLgFHx7RTuNM0Rp2oywzPE6X5pc7I3suVbhBF93/inoq+r8oq1oflFLbYbeTa1qM1fhJdK22ZXse6MaGM/miUdKnTVmbjIMZzJu0wCMKy5p07CE/UFtHVAF8TLCsdmzTKKYr1UgCh8TJjR0NDI7qOwZfGjck4vVVNo2TqRgghEk4SvRBCJJwkeiGESLi2ztGT1p/Uth17boy4vMLx/H+FysAPT2T7tgTLnGH/m2Bkg2UKdJcm6/a4MS0qHWuknpMb3QDiL4THy/dqZQ/GjUkVXY7b5hRd1t8l2/5ZqD9UHgDjmunaxJHJ5W9gotB8M4MeA1kmz1xwGD2jUXWKpaOvdc3rCvWvtL6FibdUVf0+k/VgVH8TPeN7ADfmSIGzyvqvR2zb6DyJ9WbH838ULCsrdf3hVYv2ztiQ6Z+s1KfNYlY2Op7/9WCZRfjsgQE39NzJ8fI7AOo6dWwrHGDD3yeovLb6M6qg11c/FHZy/pMgnH/qOH0BaeOYXFlHwKboirPP8UZ/CIR3jgWwkcn6b9y+FPRl1dcWQK6Qdb89XZu2JvqJNX17TNo5nn8ugAuDZQz01VRk9FfXA+n9xYHe6ovUPMSLasamcKg40NO6mHUcXNPnAwhtb+Dk/MOg5vSvFXURV11bsNXK8+R15x0DEOo/7Y3uqz4lIhotNDSOHqNxjKzpO1w9jkY5nt+NqvujQ5dSkQ2Jj5teW8fz51fHrBCfVSfIcgDpqUPGSDFrFjOT822mqpj69B+R0yHxQQDLTh8zm55netj3wSYtW4+Jl1R/XizCvgMG55rJ5ZeAKPzZO5HrpiVTN0IIkXCS6IUQIuEk0QshRMK1e2XsPwBc1Kr+i1l3AwZxR7Csa5n/R8fznzbobmch6344qlIh6w7SIEIPRdIX+EOO5+cMYo4Usu7ZBu1arjjg/oYGYQXLnOWjdzqer036qxAWHxpwjReIzSbH8z8B4EnD5k16SiLe7SqM1xzPYJdwin8LtXtlrAJxy258Bhibw49n0h4RED8mx/jfD29GKNk5hjExx//HVX2emWEAponMNrh724WIwK27b8V7xqzdQ3M6kQghhDhzkuiFECLhZm3qJv3wOz2pDnpfqFDZY8z4X9y+COgEsMhkHAT4jPgxAdrfs21vaKfE0iQfKa5dsj+6Le83ikmoiTlpqWMHV/ZHLi5qBwYXza4tkDrOfT3b9k59n/zo5DzdrF01zwRth9Vd2Rt6TkI6tUCT2XnSifs2cgM3MTPa+mZnd2dHb6iM7UxLYz6AVHd6b/j76pTqBht/eT/PQOmMBwaAQOMz/XzWEr1KpR4sa9wSLNOsLz64un933L5OPgz7vck4CtneVSbtFg75l5dhvxUsI4sfBXBrdEz3DiD8ULgRC7aP9pdL9r5gmaXxLIBr4/Y1GwoD7v0A7jdp63j+iNb21KK3lF0pAGjpB7cRmcrIOWVthxf2ET9VzLo12yM3wvH8pwDc2IyxvZelOxfcUNa6ZmfKVnIWjF1W1vZL4VLzFVoW4cbq1cmtIlM3QgiRcJLohRAi4STRCyFEwk07R5/CvMkySn8KlhHwTiaXvyGqUyL9n/Hs4tCubMzYxYATLLNAl2Zy+cVxB02kVnADc2PpobELFVWWxu2/HrbgMlP4ehBer66X+WX+fCh8oBkxFWM+q3BMADub0fcU1h/N5PJLgkUTq/ueiWqWHt63SGl1edPGoegFZiw4dUiEQw01Y96rCeFrxNpu5D7t6FAv+Ct7Z4xTYnVUoap/wluN9F8XoXvOrpki/CX0O1A4ZHqeDDXJ4PB1s6j2SwSEvzPj7dOH4MZyDC3RVb8XAs5BcIO06Vu/zODQQ1CycWUmlz8yUysm6ufamH0ALo6MCLzOQHewrMJ8USaX752uTRzatt4u3rrojel+Pm2iP/n+xNAFd7blr2RNz0eHVd8AENo6tt47Xp3c6G6zVwk29gCElN7IoPXx+68TkbGrmHUjExtbtBbA3c2ISYR8IevOuCvdGccAP8S1a5UayETW1Ux4vFnjqNi0+NDK3pG47U6+DzT8TlDPvweEyNXPpeO8AhF/OE+u1g1/DoZHb+IG+n+3KQy4oXcMzx8aW2wrPfP2ydNg5k3FrBuZsAsD7rrgMQ1CpZf5lQYi/K6Y7Qv1nx72v0yMe6Nj9n6puszxfJ8JUUn3lWLWvaKq3WoAQ9Ex3W9Vl2U8fxcTYr9KsB4q6/sBbJju5zJ1I4QQCSeJXgghEk4SvRBCJFysBVMKnNegn0bVY9DLjXXI28CI/TC2HoJ6rs5InjXa6q0ORdzgSkjaAXDkNWoEA4Vm9HMaPQHwm83oyapgj1ZoynkCwLxJa8YHYbEQngdHj+24tsZMute68pYi1bRz16nKwVDB+NIyFvih/gn8Ngwx0zNEfDhYpsCRO4V2kjpchjY6T6Xwqkk7bAbDa+C+Iqr5IsSZIMZWJtS+ASsUkmpf+8f6DVTfC0wv1dSrH/QRMP0txjCn7wr87MyhzJfvCiHEnFHvYSwBn5/Iug+0a0xzhUzdCCFEwkmiF0KIhJNEL4QQCSeJXgghEk4SvRBCJJwkeiGESDhJ9EIIkXCS6IUQIuFm7VWCQgjRSqrMj3PK+newzMLkrLyqb66TlbFCCJFw/weRJ1LNZA9BxAAAAABJRU5ErkJggg==`;export{a,r as i,u as n,n as r,d as t};
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={me:()=>e.get(`/user/me`),update:(t,n)=>e.put(`/user/${t}`,n),listAddresses:t=>e.get(`/user/${t}/address`),createAddress:(t,n)=>e.post(`/user/${t}/address`,n),updateAddress:(t,n)=>e.put(`/user/${t}/address`,n),deleteAddress:(t,n)=>e.delete(`/user/${t}/address/${n}`)};export{t};
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BLV5Ey1y.js","../chunks/DGrvnoUE.js","../chunks/xihTtKlq.js","../chunks/DTCALkcP.js","../assets/0.C0TpjoIE.css","../nodes/1.COgG0Fy8.js","../chunks/ffLBjIeN.js","../chunks/BZuNOv8h.js","../nodes/2.wASuVt48.js","../chunks/CY9OlB4q.js","../chunks/DtfiK5fE.js","../chunks/zsRwiIdn.js","../chunks/Cyrg28w2.js","../chunks/BRXBpvt8.js","../nodes/3.CHETzYMH.js","../nodes/4.DkQEi_Qd.js","../chunks/BYWIgY43.js","../chunks/BYYkuYgk.js","../nodes/5.BgmoPtaF.js","../chunks/B6JECYQD.js","../assets/5.nNRn7XR0.css","../nodes/6.B8E4TBNF.js","../chunks/Bht2nTIZ.js","../nodes/7.CJI6is1J.js","../nodes/8.Ds1ONmZq.js","../nodes/9.CYl-UmMc.js","../chunks/BeIYuf23.js","../chunks/6mSXxE57.js","../chunks/DdGy_ePN.js","../nodes/10.D6tOeXzG.js","../nodes/11.DtFjRc5K.js","../nodes/12.D1XIMXHX.js","../nodes/13.C-WGkE4P.js","../chunks/D3MDK1zR.js","../nodes/14.Zp7NcB4T.js","../nodes/15.DBuFCR43.js","../nodes/16.Bwbj6qfw.js","../nodes/17.BV0v_EXc.js","../nodes/18.e37B7XcZ.js","../nodes/19.CdvrW-UN.js","../chunks/BedKI9iq.js","../nodes/20.CPbrVrB7.js"])))=>i.map(i=>d[i]);
import{$ as e,A as t,B as n,F as r,H as i,J as a,K as o,M as s,N as c,Q as l,Z as u,a as d,at as f,c as p,dt as m,ft as h,i as g,it as _,j as v,k as y,nt as b,q as x,r as S,vt as C,w}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=c(`<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>`),j=c(`<!> <!>`,1);function M(c,g){h(g,!0);let T=d(g,`components`,23,()=>[]),E=d(g,`data_0`,3,null),D=d(g,`data_1`,3,null),O=d(g,`data_2`,3,null);a(()=>g.stores.page.set(g.page)),x(()=>{g.stores,g.page,g.constructors,T(),g.form,E(),D(),O(),g.stores.page.notify()});let k=_(!1),M=_(!1),N=_(null);S(()=>{let e=g.stores.page.subscribe(()=>{n(k)&&(b(M,!0),i().then(()=>{b(N,document.title||`untitled page`,!0)}))});return b(k,!0),e});let P=f(()=>g.constructors[2]);var F=j(),I=l(F),L=e=>{let t=f(()=>g.constructors[0]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return E()},get form(){return g.form},get params(){return g.page.params},children:(e,t)=>{var r=s(),i=l(r),a=e=>{let t=f(()=>g.constructors[1]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return D()},get form(){return g.form},get params(){return g.page.params},children:(e,t)=>{var r=s(),i=l(r);w(i,()=>n(P),(e,t)=>{p(t(e,{get data(){return O()},get form(){return g.form},get params(){return g.page.params}}),e=>T()[2]=e,()=>T()?.[2])}),v(e,r)},$$slots:{default:!0}}),e=>T()[1]=e,()=>T()?.[1])}),v(e,r)},o=e=>{let t=f(()=>g.constructors[1]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return D()},get form(){return g.form},get params(){return g.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),v(e,r)};y(i,e=>{g.constructors[2]?e(a):e(o,-1)}),v(e,r)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),v(e,r)},R=e=>{let t=f(()=>g.constructors[0]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return E()},get form(){return g.form},get params(){return g.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),v(e,r)};y(I,e=>{g.constructors[1]?e(L):e(R,-1)});var z=e(I,2),B=e=>{var i=A(),a=u(i),s=e=>{var i=r();o(()=>t(i,n(N))),v(e,i)};y(a,e=>{n(M)&&e(s)}),C(i),v(e,i)};y(z,e=>{n(k)&&e(B)}),v(c,F),m()}var N=g(M),P=[()=>O(()=>import(`../nodes/0.BLV5Ey1y.js`),__vite__mapDeps([0,1,2,3,4]),import.meta.url),()=>O(()=>import(`../nodes/1.COgG0Fy8.js`),__vite__mapDeps([5,1,2,6,7]),import.meta.url),()=>O(()=>import(`../nodes/2.wASuVt48.js`),__vite__mapDeps([8,1,7,2,3,6,9,10,11,12,13]),import.meta.url),()=>O(()=>import(`../nodes/3.CHETzYMH.js`),__vite__mapDeps([14,1,7,2,9,12]),import.meta.url),()=>O(()=>import(`../nodes/4.DkQEi_Qd.js`),__vite__mapDeps([15,1,7,2,9,16,17,10]),import.meta.url),()=>O(()=>import(`../nodes/5.BgmoPtaF.js`),__vite__mapDeps([18,1,2,6,7,16,17,10,9,19,20]),import.meta.url),()=>O(()=>import(`../nodes/6.B8E4TBNF.js`),__vite__mapDeps([21,1,7,2,9,17,10,22]),import.meta.url),()=>O(()=>import(`../nodes/7.CJI6is1J.js`),__vite__mapDeps([23,1,2]),import.meta.url),()=>O(()=>import(`../nodes/8.Ds1ONmZq.js`),__vite__mapDeps([24,1,2]),import.meta.url),()=>O(()=>import(`../nodes/9.CYl-UmMc.js`),__vite__mapDeps([25,1,2,10,7,9,12,26,27,28,19]),import.meta.url),()=>O(()=>import(`../nodes/10.D6tOeXzG.js`),__vite__mapDeps([29,1,2]),import.meta.url),()=>O(()=>import(`../nodes/11.DtFjRc5K.js`),__vite__mapDeps([30,1,2]),import.meta.url),()=>O(()=>import(`../nodes/12.D1XIMXHX.js`),__vite__mapDeps([31,1,2]),import.meta.url),()=>O(()=>import(`../nodes/13.C-WGkE4P.js`),__vite__mapDeps([32,1,2,26,10,7,9,27,28,19,33]),import.meta.url),()=>O(()=>import(`../nodes/14.Zp7NcB4T.js`),__vite__mapDeps([34,1,2]),import.meta.url),()=>O(()=>import(`../nodes/15.DBuFCR43.js`),__vite__mapDeps([35,1,2,10,7,9,27]),import.meta.url),()Line truncated
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.Bz_yxCPk.js","../chunks/DxmhE1z1.js","../chunks/xihTtKlq.js","../assets/0.tR7PeoVK.css","../nodes/1.BXecr-g5.js","../chunks/BJ9updZG.js","../chunks/DZTG_XPZ.js","../nodes/2.Du7hfnuf.js","../chunks/puscbPt7.js","../nodes/3.BTi5lIWZ.js","../nodes/4.CGofJn_s.js","../nodes/5.DeDtZvF3.js","../nodes/6.Pd88j_py.js","../chunks/BT9KdPme.js","../nodes/7.Dq6C-eoP.js","../nodes/8.BW4HeEmf.js","../nodes/9.BK5CBBXk.js","../nodes/10.3_h0i6QI.js","../nodes/11.Q5ro3K8Z.js","../nodes/12.CqhEZfs2.js","../nodes/13.CyYsvdDt.js","../nodes/14.DZ-KMWs7.js","../nodes/15.CzF_DBRn.js","../nodes/16.DRUykhSC.js","../nodes/17.KofPYOuZ.js","../chunks/Nqp1gr6g.js","../nodes/18.3mUWXX_j.js"])))=>i.map(i=>d[i]);
import{A as e,C as t,F as n,H as r,I as i,K as a,P as o,Q as s,T as c,V as l,_ as u,a as d,f,g as p,h as m,i as h,j as g,k as _,m as v,n as y,q as b,r as x,v as S,y as C,z as w}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=S(`<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>`),j=S(`<!> <!>`,1);function M(x,S){b(S,!0);let T=h(S,`components`,23,()=>[]),E=h(S,`data_0`,3,null),D=h(S,`data_1`,3,null),O=h(S,`data_2`,3,null);g(()=>S.stores.page.set(S.page)),e(()=>{S.stores,S.page,S.constructors,T(),S.form,E(),D(),O(),S.stores.page.notify()});let k=l(!1),M=l(!1),N=l(null);y(()=>{let e=S.stores.page.subscribe(()=>{t(k)&&(w(M,!0),c().then(()=>{w(N,document.title||`untitled page`,!0)}))});return w(k,!0),e});let P=r(()=>S.constructors[2]);var F=j(),I=n(F),L=e=>{let i=r(()=>S.constructors[0]);var a=u(),o=n(a);f(o,()=>t(i),(e,i)=>{d(i(e,{get data(){return E()},get form(){return S.form},get params(){return S.page.params},children:(e,i)=>{var a=u(),o=n(a),s=e=>{let i=r(()=>S.constructors[1]);var a=u(),o=n(a);f(o,()=>t(i),(e,r)=>{d(r(e,{get data(){return D()},get form(){return S.form},get params(){return S.page.params},children:(e,r)=>{var i=u(),a=n(i);f(a,()=>t(P),(e,t)=>{d(t(e,{get data(){return O()},get form(){return S.form},get params(){return S.page.params}}),e=>T()[2]=e,()=>T()?.[2])}),p(e,i)},$$slots:{default:!0}}),e=>T()[1]=e,()=>T()?.[1])}),p(e,a)},c=e=>{let i=r(()=>S.constructors[1]);var a=u(),o=n(a);f(o,()=>t(i),(e,t)=>{d(t(e,{get data(){return D()},get form(){return S.form},get params(){return S.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),p(e,a)};v(o,e=>{S.constructors[2]?e(s):e(c,-1)}),p(e,a)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),p(e,a)},R=e=>{let i=r(()=>S.constructors[0]);var a=u(),o=n(a);f(o,()=>t(i),(e,t)=>{d(t(e,{get data(){return E()},get form(){return S.form},get params(){return S.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),p(e,a)};v(I,e=>{S.constructors[1]?e(L):e(R,-1)});var z=i(I,2),B=e=>{var n=A(),r=o(n),i=e=>{var n=C();_(()=>m(n,t(N))),p(e,n)};v(r,e=>{t(M)&&e(i)}),s(n),p(e,n)};v(z,e=>{t(k)&&e(B)}),p(x,F),a()}var N=x(M),P=[()=>O(()=>import(`../nodes/0.Bz_yxCPk.js`),__vite__mapDeps([0,1,2,3]),import.meta.url),()=>O(()=>import(`../nodes/1.BXecr-g5.js`),__vite__mapDeps([4,1,2,5,6]),import.meta.url),()=>O(()=>import(`../nodes/2.Du7hfnuf.js`),__vite__mapDeps([7,1,6,2,5,8]),import.meta.url),()=>O(()=>import(`../nodes/3.BTi5lIWZ.js`),__vite__mapDeps([9,1,2]),import.meta.url),()=>O(()=>import(`../nodes/4.CGofJn_s.js`),__vite__mapDeps([10,1,2]),import.meta.url),()=>O(()=>import(`../nodes/5.DeDtZvF3.js`),__vite__mapDeps([11,1,2]),import.meta.url),()=>O(()=>import(`../nodes/6.Pd88j_py.js`),__vite__mapDeps([12,1,2,13]),import.meta.url),()=>O(()=>import(`../nodes/7.Dq6C-eoP.js`),__vite__mapDeps([14,1,2]),import.meta.url),()=>O(()=>import(`../nodes/8.BW4HeEmf.js`),__vite__mapDeps([15,1,2]),import.meta.url),()=>O(()=>import(`../nodes/9.BK5CBBXk.js`),__vite__mapDeps([16,1,2]),import.meta.url),()=>O(()=>import(`../nodes/10.3_h0i6QI.js`),__vite__mapDeps([17,1,2]),import.meta.url),()=>O(()=>import(`../nodes/11.Q5ro3K8Z.js`),__vite__mapDeps([18,1,2]),import.meta.url),()=>O(()=>import(`../nodes/12.CqhEZfs2.js`),__vite__mapDeps([19,1,2]),import.meta.url),()=>O(()=>import(`../nodes/13.CyYsvdDt.js`),__vite__mapDeps([20,1,2]),import.meta.url),()=>O(()=>import(`../nodes/14.DZ-KMWs7.js`),__vite__mapDeps([21,1,2]),import.meta.url),()=>O(()=>import(`../nodes/15.CzF_DBRn.js`),__vite__mapDeps([22,1,2]),import.meta.url),()=>O(()=>import(`../nodes/16.DRUykhSC.js`),__vite__mapDeps([23,1,2]),import.meta.url),()=>O(()=>import(`../nodes/17.KofPYOuZ.js`)Line truncated
@@ -0,0 +1 @@
import{c as e,r as t}from"../chunks/BZuNOv8h.js";export{e as load_css,t as start};
@@ -1 +0,0 @@
import{n as e,s as t}from"../chunks/DZTG_XPZ.js";export{t as load_css,e as start};
@@ -0,0 +1 @@
import{G as e,K as t,M as n,N as r,Q as i,T as a,Y as o,_t as s,bt as c,dt as l,ft as u,j as d,m as f,r as p,x as m}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";import{t as h}from"../chunks/DTCALkcP.js";var g=c({prerender:()=>!1,ssr:()=>!1}),_=`data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMTNtbSIKICAgaGVpZ2h0PSIxM21tIgogICB2aWV3Qm94PSIwIDAgMTMgMTMiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIGlua3NjYXBlOnZlcnNpb249IjEuNC40IChkY2FmM2U3ZDllLCAyMDI2LTA1LTA1KSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZmF2aWNvbi5zdmciCiAgIGlua3NjYXBlOmV4cG9ydC1maWxlbmFtZT0iLi4vYmFja2VuZC93ZWIvc3JjL2xpYi9hc3NldHMvbG9nby10eHQucG5nIgogICBpbmtzY2FwZTpleHBvcnQteGRwaT0iOTYiCiAgIGlua3NjYXBlOmV4cG9ydC15ZHBpPSI5NiIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzEiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6em9vbT0iMS40NDMxODE4IgogICAgIGlua3NjYXBlOmN4PSIyODcuOTA1NTIiCiAgICAgaW5rc2NhcGU6Y3k9IjIxNC44MDMxNSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAxMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImxheWVyMSIgLz4KICA8ZGVmcwogICAgIGlkPSJkZWZzMSIgLz4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJDYWxxdWUgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjE0LjExMTFweDtmb250LWZhbWlseTpwaXhlbGFydDstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOnBpeGVsYXJ0O3dyaXRpbmctbW9kZTpsci10YjtkaXJlY3Rpb246bHRyO2ZpbGw6IzM3YWJjODtzdHJva2Utd2lkdGg6MC4yNjQ1ODMiCiAgICAgICB4PSIyLjc0OTk5OTUiCiAgICAgICB5PSIxMS41NDk5OTciCiAgICAgICBpZD0idGV4dDEiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW4xIgogICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LXNpemU6MTQuMTExMXB4O2ZvbnQtZmFtaWx5OidHb2h1Rm9udCAxMSBOZXJkIEZvbnQnOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246J0dvaHVGb250IDExIE5lcmQgRm9udCc7ZmlsbDojMGVhNWU5O2ZpbGwtb3BhY2l0eToxO3N0cm9rZS13aWR0aDowLjI2NDU4MyIKICAgICAgICAgeD0iMi43NDk5OTk1IgogICAgICAgICB5PSIxMS41NDk5OTciPmw8L3RzcGFuPjwvdGV4dD4KICA8L2c+Cjwvc3ZnPgo=`,v=r(`<link rel="icon"/> <link rel="stylesheet" href="/assets/fontello/css/fontello.css"/>`,1);function y(r,c){u(c,!0),p(()=>{h()});var g=n();m(`12qhfyh`,n=>{var r=v(),a=i(r);s(2),t(()=>f(a,`href`,_)),e(()=>{o.title=`Lasssistanoque`}),d(n,r)});var y=i(g);a(y,()=>c.children),d(r,g),l()}export{y as component,g as universal};
@@ -1 +0,0 @@
import{$ as e,F as t,M as n,O as r,Z as i,_ as a,c as o,d as s,g as c,k as l,p as u,v as d}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var f=e({prerender:()=>!1,ssr:()=>!1}),p=`data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMTNtbSIKICAgaGVpZ2h0PSIxM21tIgogICB2aWV3Qm94PSIwIDAgMTMgMTMiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIGlua3NjYXBlOnZlcnNpb249IjEuNC40IChkY2FmM2U3ZDllLCAyMDI2LTA1LTA1KSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZmF2aWNvbi5zdmciCiAgIGlua3NjYXBlOmV4cG9ydC1maWxlbmFtZT0iLi4vYmFja2VuZC93ZWIvc3JjL2xpYi9hc3NldHMvbG9nby10eHQucG5nIgogICBpbmtzY2FwZTpleHBvcnQteGRwaT0iOTYiCiAgIGlua3NjYXBlOmV4cG9ydC15ZHBpPSI5NiIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzEiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6em9vbT0iMS40NDMxODE4IgogICAgIGlua3NjYXBlOmN4PSIyODcuOTA1NTIiCiAgICAgaW5rc2NhcGU6Y3k9IjIxNC44MDMxNSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAxMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImxheWVyMSIgLz4KICA8ZGVmcwogICAgIGlkPSJkZWZzMSIgLz4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJDYWxxdWUgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjE0LjExMTFweDtmb250LWZhbWlseTpwaXhlbGFydDstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOnBpeGVsYXJ0O3dyaXRpbmctbW9kZTpsci10YjtkaXJlY3Rpb246bHRyO2ZpbGw6IzM3YWJjODtzdHJva2Utd2lkdGg6MC4yNjQ1ODMiCiAgICAgICB4PSIyLjc0OTk5OTUiCiAgICAgICB5PSIxMS41NDk5OTciCiAgICAgICBpZD0idGV4dDEiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW4xIgogICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LXNpemU6MTQuMTExMXB4O2ZvbnQtZmFtaWx5OidHb2h1Rm9udCAxMSBOZXJkIEZvbnQnOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246J0dvaHVGb250IDExIE5lcmQgRm9udCc7ZmlsbDojMGVhNWU5O2ZpbGwtb3BhY2l0eToxO3N0cm9rZS13aWR0aDowLjI2NDU4MyIKICAgICAgICAgeD0iMi43NDk5OTk1IgogICAgICAgICB5PSIxMS41NDk5OTciPmw8L3RzcGFuPjwvdGV4dD4KICA8L2c+Cjwvc3ZnPgo=`,m=d(`<link rel="icon"/> <link rel="stylesheet" href="/assets/fontello/css/fontello.css"/>`,1);function h(e,d){var f=a();s(`12qhfyh`,e=>{var a=m(),s=t(a);i(2),l(()=>o(s,`href`,p)),r(()=>{n.title=`Lasssistanoque`}),c(e,a)});var h=t(f);u(h,()=>d.children),c(e,f)}export{h as component,f as universal};
@@ -1 +0,0 @@
import{F as e,I as t,K as n,P as r,Q as i,g as a,h as o,k as s,q as c,v as l}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";import{t as u}from"../chunks/BJ9updZG.js";var d=l(`<h1> </h1> <p> </p>`,1);function f(l,f){c(f,!0);var p=d(),m=e(p),h=r(m,!0);i(m);var g=t(m,2),_=r(g,!0);i(g),s(()=>{o(h,u.status),o(_,u.error?.message)}),a(l,p),n()}export{f as component};
@@ -0,0 +1 @@
import{$ as e,A as t,K as n,N as r,Q as i,Z as a,dt as o,ft as s,j as c,vt as l}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";import{t as u}from"../chunks/ffLBjIeN.js";var d=r(`<h1> </h1> <p> </p>`,1);function f(r,f){s(f,!0);var p=d(),m=i(p),h=a(m,!0);l(m);var g=e(m,2),_=a(g,!0);l(g),n(()=>{t(h,u.status),t(_,u.error?.message)}),c(r,p),o()}export{f as component};
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Base de connaissance`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
@@ -0,0 +1 @@
import{N as e,bt as t,j as n}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var r=t({load:()=>i});function i(){return{title:`Paramétrage : Canaux de communication`,back:`/settings`}}var a=e(`<div>Modèles</div>`);function o(e){var t=a();n(e,t)}export{o as component,r as universal};
@@ -0,0 +1 @@
import{N as e,bt as t,j as n}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var r=t({load:()=>i});function i(){return{title:`Paramétrage : Base de connaissance`,back:`/settings`}}var a=e(`<div>Modèles</div>`);function o(e){var t=a();n(e,t)}export{o as component,r as universal};
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Mémoire`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Modèles`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
@@ -0,0 +1 @@
import{N as e,bt as t,j as n}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var r=t({load:()=>i});function i(){return{title:`Paramétrage : Mémoire`,back:`/settings`}}var a=e(`<div>Modèles</div>`);function o(e){var t=a();n(e,t)}export{o as component,r as universal};
@@ -0,0 +1 @@
import{$ as e,A as t,B as n,E as r,G as i,I as a,K as o,L as s,M as c,N as l,O as u,Q as d,R as f,U as p,X as m,Z as h,_t as g,a as _,b as v,bt as y,dt as b,et as x,ft as S,it as C,j as w,k as T,l as E,nt as D,p as O,u as k,vt as A}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";import{t as j}from"../chunks/BeIYuf23.js";import{n as M,t as N}from"../chunks/6mSXxE57.js";import{t as P}from"../chunks/DdGy_ePN.js";import{n as F}from"../chunks/D3MDK1zR.js";var I=y({load:()=>L}),L=async({params:e})=>({title:`Paramétrage : Modèles`,back:`../settings`,space:e.space,providerTypes:await j.providerTypes(),providers:await j.getProviders(e.space)}),R=l(`<li><label><input type="checkbox"/> </label></li>`),z=l(`<div><div class="formgroup my-2"><form><div class="formelement"><label for="name">Nom</label> <input id="name" type="text" class="textinput" placeholder="nom affiché"/> <!></div> <div class="formelement"><label for="type" class="block my-1 flex-1">Type</label> <!> <!></div> <div class="formelement"><label for="baseUrl">URL</label> <input id="baseUrl" type="text" class="textinput" placeholder="http://127.0.0.1:8080/v1" autocomplete="off"/> <!></div> <div class="formelement"><label for="apiKey">Clé API</label> <input id="apiKey" type="password" class="textinput" placeholder="clé API (optionnel)" autocomplete="new-password"/> <!></div> <div class="formelement"><label for="models">Models</label> <button class="bt small">Afficher les modèles disponibles</button> <ol class="ml-2"></ol> <div><!></div></div> <div class="text-right"><button type="button" class="bt">Annuler</button> <button type="submit" class="bt mx-4">Enregistrer</button></div></form></div></div>`);function B(a,l){S(l,!0);let u=_(l,`item`,7),p=C(x(u().models.split(`|`))),m=C(x(u().models.split(`|`).map(e=>({value:e,selected:!0})))),y=new M,F=e=>{e.focus()},I=e=>{e.preventDefault(),u().models=n(p).join(`|`),l.onSave(u())},L=e=>{e.preventDefault(),l.onCancel()},B=e=>{e.preventDefault(),y.clear(),j.getAvailableModels(u().teamId,u()).then(e=>{D(m,e.map(e=>({value:e,selected:n(p).includes(e)})),!0)}).catch(e=>{y.set(`models`,e)})},V=()=>{D(p,n(m).filter(e=>e.selected).map(e=>e.value),!0)};var H=z(),U=h(H),W=h(U),G=h(W),K=e(h(G),2);O(K),i(()=>k(K,()=>u().name,e=>u().name=e)),v(K,e=>F?.(e));var ee=e(K,2);N(ee,{key:`name`,get errors(){return l.errors}}),A(G);var q=e(G,2),te=e(h(q),2);P(te,{get items(){return l.providerTypes},valueMode:`id`,get value(){return u().type},set value(e){u().type=e}});var ne=e(te,2);N(ne,{key:`type`,get errors(){return l.errors}}),A(q);var J=e(q,2),Y=e(h(J),2);O(Y);var re=e(Y,2);N(re,{key:`baseUrl`,get errors(){return l.errors}}),A(J);var X=e(J,2),Z=e(h(X),2);O(Z);var ie=e(Z,2);N(ie,{key:`apiKey`,get errors(){return l.errors}}),A(X);var Q=e(X,2),ae=e(h(Q),2),$=e(ae,2);r($,21,()=>n(m),e=>e.value,(r,i,a)=>{var l=c(),u=d(l),f=r=>{var a=R(),c=h(a),l=h(c);O(l);var u=e(l);A(c),A(a),o(()=>t(u,` ${n(i).value??``}`)),s(`change`,l,V),E(l,()=>n(i).selected,e=>n(i).selected=e),w(r,a)};T(u,e=>{n(i).value&&e(f)}),w(r,l)}),A($);var oe=e($,2),se=h(oe);N(se,{key:`models`,get errors(){return y}}),A(oe),A(Q);var ce=e(Q,2),le=h(ce);g(2),A(ce),A(W),A(U),A(H),f(`submit`,W,I),s(`change`,W,()=>{l.errors.clear()}),k(Y,()=>u().baseUrl,e=>u().baseUrl=e),k(Z,()=>u().apiKey,e=>u().apiKey=e),s(`click`,ae,B),s(`click`,le,L),w(a,H),b()}a([`change`,`click`]);var V=l(`<button class="rounded w-full border border-dashed p-2 border-stone-500 cursor-pointer" tabindex="0"><i class="icon-plus"></i> Ajouter un fournisseur</button>`),H=l(`<div class="rounded border p-2 my-2 border-stone-500 flex formgroup"><div class="flex-1 p-2"><span> </span> <span class="text-stone-500"> </span></div> <button class="bt small mx-1" aria-label="Modifier"><i class="icon-edit"></i></button> <button class="bt small mx-1" aria-label="Supprimer"><i class="icon-trash-empty"></i></button></div>`),U=l(`<div class="container"><!> <!></div>`);function W(i,a){S(a,!0);let l=C(x(p(()=>a.data.providers))),g=C(!1),_=C(null),v=C(x({})),y=new M,E=()=>{n(_)||(y.clear(),D(v,{},!0),n(v).models=``,D(g,!0))},O=()=>{D(g,!1)},k=e=>{e.name||y.set(`name`,`Le nom est obligatoire !`),e.type||y.set(`type`,`Le type est obligatoire !`),e.baseUrl&&!F(e.baseUrl)&&y.set(`baseUrl`,`Mauvaise URL !`),y.size===0&&j.createProvider(a.data.space,e).then(e=>{n(l).push(e),D(g,!1),D(v,{},!0)}).catch(e=>{console.log(e)})},N=()=>{D(_,null),D(v,{},!0)},P=e=>{e.name||y.set(`name`,`Le nom est obligatoire !`),e.type||y.set(`type`,`Le type est obligatoire !`),e.baseUrl&&!F(e.baseUrl)&&y.set(`baseUrl`,`Mauvaise URL !`),y.size===0&&j.updateProvider(a.data.space,e).then(e=>{D(l,n(l).map(t=>e.id===t.id?e:t),!0),D(_,null),D(v,{},!0)}).catch(e=>{console.log(e)})},I=async e=>{y.clear(),!n(g)&&j.getProvider(a.data.space,e).then(t=>{D(l,n(l).map(n=>n.id===e?t:n),!0),D(_,e,!0)})},L=e=>{j.deleteProvider(a.data.space,e).then(()=>{D(l,n(l).filter(t=>t.id!==e),!0)}).catch(e=>{console.log(e)})},R=e=>{e.key===`Escape`&&N()};var z=U();f(`keydown Line truncated
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Skills`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Outils`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
Loaded 100 of 239 files, more files were not shown because too many files have changed in this diff. Show more