Chat persistant pour la partie lore et la partie campagne pour chaque page / scène.....
Correction du carroussel Passage en v0.4.0 Correction du docker compose pour tout le temps utiliser le bon port que ce soit prod ou dev
This commit is contained in:
@@ -16,7 +16,19 @@ export interface ChatMessage {
|
||||
* - done : le stream s'est terminé proprement (l'observable va compléter).
|
||||
* - error : une erreur s'est produite côté serveur (l'observable va erreur-compléter).
|
||||
*/
|
||||
/**
|
||||
* Instantané d'occupation de la fenêtre de contexte (émis 1x par tour, avant le streaming).
|
||||
* Les valeurs sont exprimées en tokens (~cl100k_base, ±10% vs tokenizer natif du modèle).
|
||||
*/
|
||||
export interface ChatUsage {
|
||||
system: number;
|
||||
history: number;
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export type ChatStreamEvent =
|
||||
| { type: 'usage'; usage: ChatUsage }
|
||||
| { type: 'token'; value: string }
|
||||
| { type: 'done' }
|
||||
| { type: 'error'; message: string };
|
||||
@@ -128,12 +140,19 @@ export class AiChatService {
|
||||
|
||||
const dispatchCurrentEvent = () => {
|
||||
const eventName = currentEvent ?? 'message';
|
||||
// DEBUG jauge de contexte — à retirer une fois stabilisé.
|
||||
if (eventName !== 'message') {
|
||||
console.log('[AiChatService] SSE event:', eventName, 'data:', currentData);
|
||||
}
|
||||
if (eventName === 'error') {
|
||||
const message = this.safeParseMessage(currentData);
|
||||
subscriber.error(new Error(message));
|
||||
} else if (eventName === 'done') {
|
||||
subscriber.next({ type: 'done' });
|
||||
subscriber.complete();
|
||||
} else if (eventName === 'usage') {
|
||||
const usage = this.safeParseUsage(currentData);
|
||||
if (usage) subscriber.next({ type: 'usage', usage });
|
||||
} else {
|
||||
// Événement 'message' (défaut) : JSON {"token": "..."}
|
||||
const token = this.safeParseToken(currentData);
|
||||
@@ -188,6 +207,23 @@ export class AiChatService {
|
||||
}
|
||||
}
|
||||
|
||||
private safeParseUsage(json: string): ChatUsage | null {
|
||||
try {
|
||||
const obj = JSON.parse(json) as Partial<ChatUsage>;
|
||||
if (
|
||||
typeof obj.system === 'number' &&
|
||||
typeof obj.history === 'number' &&
|
||||
typeof obj.current === 'number' &&
|
||||
typeof obj.max === 'number'
|
||||
) {
|
||||
return { system: obj.system, history: obj.history, current: obj.current, max: obj.max };
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private safeParseMessage(json: string): string {
|
||||
try {
|
||||
const obj = JSON.parse(json) as { message?: string };
|
||||
|
||||
35
web/src/app/services/conversation.model.ts
Normal file
35
web/src/app/services/conversation.model.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type ConversationRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
export interface ConversationMessage {
|
||||
id?: string;
|
||||
role: ConversationRole;
|
||||
content: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
loreId?: string | null;
|
||||
campaignId?: string | null;
|
||||
entityType?: string | null;
|
||||
entityId?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
messages?: ConversationMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre strict pour le listing sidebar. Fournir soit loreId soit campaignId.
|
||||
* entityType + entityId vont ensemble — tous deux null = niveau racine.
|
||||
*/
|
||||
export interface ConversationContext {
|
||||
loreId?: string | null;
|
||||
campaignId?: string | null;
|
||||
entityType?: 'page' | 'arc' | 'chapter' | 'scene' | null;
|
||||
entityId?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateConversationPayload extends ConversationContext {
|
||||
title?: string;
|
||||
}
|
||||
64
web/src/app/services/conversation.service.ts
Normal file
64
web/src/app/services/conversation.service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
Conversation,
|
||||
ConversationContext,
|
||||
ConversationMessage,
|
||||
CreateConversationPayload,
|
||||
} from './conversation.model';
|
||||
|
||||
/**
|
||||
* Service HTTP des conversations persistees.
|
||||
*
|
||||
* Le streaming (chat/stream) reste pris en charge par AiChatService. Ce
|
||||
* service ne gere que la persistance (metadonnees + messages) et
|
||||
* l'auto-titre declenche apres le 1er echange.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConversationService {
|
||||
private readonly apiUrl = 'http://localhost:8080/api/conversations';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
list(ctx: ConversationContext): Observable<Conversation[]> {
|
||||
let params = new HttpParams();
|
||||
if (ctx.loreId) params = params.set('loreId', ctx.loreId);
|
||||
if (ctx.campaignId) params = params.set('campaignId', ctx.campaignId);
|
||||
if (ctx.entityType) params = params.set('entityType', ctx.entityType);
|
||||
if (ctx.entityId) params = params.set('entityId', ctx.entityId);
|
||||
return this.http.get<Conversation[]>(this.apiUrl, { params });
|
||||
}
|
||||
|
||||
getById(id: string): Observable<Conversation> {
|
||||
return this.http.get<Conversation>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
create(payload: CreateConversationPayload): Observable<Conversation> {
|
||||
return this.http.post<Conversation>(this.apiUrl, payload);
|
||||
}
|
||||
|
||||
rename(id: string, title: string): Observable<void> {
|
||||
return this.http.patch<void>(`${this.apiUrl}/${id}/title`, { title });
|
||||
}
|
||||
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
appendMessage(
|
||||
id: string,
|
||||
role: 'user' | 'assistant' | 'system',
|
||||
content: string,
|
||||
): Observable<ConversationMessage> {
|
||||
return this.http.post<ConversationMessage>(`${this.apiUrl}/${id}/messages`, {
|
||||
role,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
/** Declenche la generation auto du titre cote Brain. */
|
||||
autoTitle(id: string): Observable<{ title: string }> {
|
||||
return this.http.post<{ title: string }>(`${this.apiUrl}/${id}/auto-title`, {});
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export interface AppSettings {
|
||||
llm_model: string;
|
||||
onemin_model: string;
|
||||
onemin_api_key_set: boolean;
|
||||
llm_num_ctx: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,13 @@ export interface AppSettingsUpdate {
|
||||
llm_model?: string;
|
||||
onemin_model?: string;
|
||||
onemin_api_key?: string;
|
||||
llm_num_ctx?: number;
|
||||
}
|
||||
|
||||
/** Metadonnees d'un modele Ollama (issues de /api/show). */
|
||||
export interface OllamaModelInfo {
|
||||
/** Fenetre de contexte max du modele (en tokens). 0 si inconnue. */
|
||||
context_length: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -49,6 +57,11 @@ export class SettingsService {
|
||||
return this.http.get<{ models: string[] }>(`${this.apiUrl}/models/ollama`, this.authOptions);
|
||||
}
|
||||
|
||||
getOllamaModelInfo(name: string): Observable<OllamaModelInfo> {
|
||||
return this.http.post<OllamaModelInfo>(
|
||||
`${this.apiUrl}/models/ollama/info`, { name }, this.authOptions);
|
||||
}
|
||||
|
||||
listOneMinModels(): Observable<{ groups: OneMinModelGroup[] }> {
|
||||
return this.http.get<{ groups: OneMinModelGroup[] }>(`${this.apiUrl}/models/onemin`, this.authOptions);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user