Plusieurs gros ajouts :
- Possibilité de discuter avec un PDF ; RAG ou analyse approfondie. Enlèvement de l'autre outil PDF de discussion qui analysait d'abord un PDF en proposant directement une intégration sans attendre qu'on pose de question - Mise en place de l'import directement dans les outils dans la sidebar - Mise en place d'un outil pour créer des tables aléatoires avec possibilité d'utiliser pendant la partie - Mise en place d'un outil pour mettre en place des PNJ, scènes, chapitre.... directement à partir de la discussion avec le PDF - Mise en place RAG avec mistal-embeding ou nomic si on utilise ollama - Mise en place mistral, google en fournisseurs alternatifs pour l'IA dans le cloud - version 0.11.0-bêta
This commit is contained in:
126
web/src/app/services/notebook.service.ts
Normal file
126
web/src/app/services/notebook.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Injectable, NgZone } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Notebook, NotebookDetail, NotebookSource, NotebookChatEvent } from './notebook.model';
|
||||
|
||||
/**
|
||||
* Service des notebooks (atelier RAG) : CRUD, upload/indexation de sources,
|
||||
* et chat ancré STREAMÉ (SSE via fetch, comme ai-chat.service).
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotebookService {
|
||||
private readonly apiUrl = '/api/notebooks';
|
||||
|
||||
constructor(private http: HttpClient, private zone: NgZone) {}
|
||||
|
||||
listByCampaign(campaignId: string): Observable<Notebook[]> {
|
||||
return this.http.get<Notebook[]>(`${this.apiUrl}/campaign/${campaignId}`);
|
||||
}
|
||||
|
||||
get(id: string): Observable<NotebookDetail> {
|
||||
return this.http.get<NotebookDetail>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
create(campaignId: string, name: string): Observable<Notebook> {
|
||||
return this.http.post<Notebook>(this.apiUrl, { campaignId, name });
|
||||
}
|
||||
|
||||
rename(id: string, name: string): Observable<Notebook> {
|
||||
return this.http.put<Notebook>(`${this.apiUrl}/${id}`, { name });
|
||||
}
|
||||
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
/** Upload + indexation d'une source (multipart). Bloquant côté serveur (peut être long). */
|
||||
addSource(notebookId: string, file: File): Observable<NotebookSource> {
|
||||
const form = new FormData();
|
||||
form.append('file', file, file.name);
|
||||
return this.http.post<NotebookSource>(`${this.apiUrl}/${notebookId}/sources`, form);
|
||||
}
|
||||
|
||||
deleteSource(sourceId: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/sources/${sourceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat ancré streamé. fetch() + ReadableStream (HttpClient bufferise les SSE).
|
||||
* Émissions forcées dans la zone Angular pour la détection de changement.
|
||||
*/
|
||||
streamChat(notebookId: string, message: string, deep = false): Observable<NotebookChatEvent> {
|
||||
return new Observable<NotebookChatEvent>((subscriber) => {
|
||||
const controller = new AbortController();
|
||||
const emit = (ev: NotebookChatEvent) => this.zone.run(() => subscriber.next(ev));
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/${notebookId}/chat/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ message, deep }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok || !response.body) {
|
||||
emit({ type: 'error', message: `HTTP ${response.status}` });
|
||||
this.zone.run(() => subscriber.complete());
|
||||
return;
|
||||
}
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let currentEvent: string | null = null;
|
||||
let currentData = '';
|
||||
|
||||
const dispatch = () => {
|
||||
const name = currentEvent ?? 'message';
|
||||
if (name === 'token') {
|
||||
try { emit({ type: 'token', value: (JSON.parse(currentData) as { token?: string }).token ?? '' }); }
|
||||
catch { /* ignore */ }
|
||||
} else if (name === 'progress') {
|
||||
try {
|
||||
const o = JSON.parse(currentData) as { current?: number; total?: number };
|
||||
emit({ type: 'progress', current: o.current ?? 0, total: o.total ?? 0 });
|
||||
} catch { /* ignore */ }
|
||||
} else if (name === 'done') {
|
||||
emit({ type: 'done' });
|
||||
} else if (name === 'error') {
|
||||
let msg = 'Erreur du chat.';
|
||||
try { msg = (JSON.parse(currentData) as { message?: string }).message ?? msg; } catch { /* garde */ }
|
||||
emit({ type: 'error', message: msg });
|
||||
}
|
||||
currentEvent = null;
|
||||
currentData = '';
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let idx: number;
|
||||
while ((idx = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, idx).replace(/\r$/, '');
|
||||
buffer = buffer.slice(idx + 1);
|
||||
if (line === '') { if (currentEvent !== null || currentData !== '') dispatch(); continue; }
|
||||
if (line.startsWith('event:')) currentEvent = line.slice(6).trim();
|
||||
else if (line.startsWith('data:')) {
|
||||
const chunk = line.slice(5).replace(/^ /, '');
|
||||
currentData = currentData ? `${currentData}\n${chunk}` : chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentEvent !== null || currentData !== '') dispatch();
|
||||
this.zone.run(() => subscriber.complete());
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
emit({ type: 'error', message: (err as Error).message || 'Erreur réseau' });
|
||||
}
|
||||
this.zone.run(() => subscriber.complete());
|
||||
}
|
||||
})();
|
||||
|
||||
return () => controller.abort();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user