Compare commits
2 Commits
v0.12.4-be
...
v0.12.5-be
| Author | SHA1 | Date | |
|---|---|---|---|
| bc0cbb0f7b | |||
| 6740ed2177 |
@@ -26,7 +26,7 @@ from app.infrastructure.ollama_model_installer import ensure_ollama_embedding_mo
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="LoreMind Brain",
|
title="LoreMind Brain",
|
||||||
description="Backend IA pour la génération de contenu narratif.",
|
description="Backend IA pour la génération de contenu narratif.",
|
||||||
version="0.12.4-beta",
|
version="0.12.5-beta",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<groupId>com.loremind</groupId>
|
<groupId>com.loremind</groupId>
|
||||||
<artifactId>loremind-core</artifactId>
|
<artifactId>loremind-core</artifactId>
|
||||||
<version>0.12.4-beta</version>
|
<version>0.12.5-beta</version>
|
||||||
<name>LoreMind Core</name>
|
<name>LoreMind Core</name>
|
||||||
<description>Backend Core - Architecture Hexagonale</description>
|
<description>Backend Core - Architecture Hexagonale</description>
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,18 @@ public class NotebookController {
|
|||||||
List<NotebookChatStreamer.Msg> history = service.getMessages(id).stream()
|
List<NotebookChatStreamer.Msg> history = service.getMessages(id).stream()
|
||||||
.map(m -> new NotebookChatStreamer.Msg(m.getRole(), m.getContent()))
|
.map(m -> new NotebookChatStreamer.Msg(m.getRole(), m.getContent()))
|
||||||
.toList();
|
.toList();
|
||||||
List<String> sourceIds = service.readySourceIds(id);
|
// Sélection de l'UI (cases cochées) : on ne garde que les sources qui
|
||||||
|
// appartiennent bien à CE notebook et sont prêtes — un id étranger est
|
||||||
|
// ignoré. Limite le coût (ex. analyse approfondie sur 1 PDF au lieu de 5).
|
||||||
|
// Variable finale : elle est capturée par la lambda du taskExecutor.
|
||||||
|
List<String> readyIds = service.readySourceIds(id);
|
||||||
|
final List<String> sourceIds;
|
||||||
|
if (req.sourceIds() != null) {
|
||||||
|
var wanted = new java.util.HashSet<>(req.sourceIds());
|
||||||
|
sourceIds = readyIds.stream().filter(wanted::contains).toList();
|
||||||
|
} else {
|
||||||
|
sourceIds = readyIds;
|
||||||
|
}
|
||||||
String context = service.buildContext(nb.getCampaignId());
|
String context = service.buildContext(nb.getCampaignId());
|
||||||
|
|
||||||
boolean deep = req.deep() != null && req.deep();
|
boolean deep = req.deep() != null && req.deep();
|
||||||
@@ -220,5 +231,10 @@ public class NotebookController {
|
|||||||
|
|
||||||
public record CreateRequest(String campaignId, String name) {}
|
public record CreateRequest(String campaignId, String name) {}
|
||||||
public record RenameRequest(String name) {}
|
public record RenameRequest(String name) {}
|
||||||
public record ChatRequest(String message, Boolean deep) {}
|
/**
|
||||||
|
* @param sourceIds Optionnel : sous-ensemble de sources à utiliser pour ce tour
|
||||||
|
* (cases cochées dans l'UI). Null = toutes les sources prêtes.
|
||||||
|
* Toujours intersecté avec les sources du notebook (sécurité).
|
||||||
|
*/
|
||||||
|
public record ChatRequest(String message, Boolean deep, List<String> sourceIds) {}
|
||||||
}
|
}
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.12.4-beta",
|
"version": "0.12.5-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.12.4-beta",
|
"version": "0.12.5-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^21.2.16",
|
"@angular/animations": "^21.2.16",
|
||||||
"@angular/common": "^21.2.16",
|
"@angular/common": "^21.2.16",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.12.4-beta",
|
"version": "0.12.5-beta",
|
||||||
"description": "LoreMind Frontend - Angular",
|
"description": "LoreMind Frontend - Angular",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -20,13 +20,30 @@
|
|||||||
@if (uploadError) {
|
@if (uploadError) {
|
||||||
<p class="nbd-upload-error">{{ uploadError }}</p>
|
<p class="nbd-upload-error">{{ uploadError }}</p>
|
||||||
}
|
}
|
||||||
|
@if (readySourceCount > 1) {
|
||||||
|
<p class="nbd-sources-hint"
|
||||||
|
[class.partial]="selectedSourceIds.size < readySourceCount">
|
||||||
|
{{ selectedSourceIds.size }}/{{ readySourceCount }} source(s) utilisée(s) par le chat et l'analyse approfondie
|
||||||
|
</p>
|
||||||
|
}
|
||||||
@for (s of sources; track s) {
|
@for (s of sources; track s) {
|
||||||
<div class="nbd-source">
|
<div class="nbd-source" [class.nbd-source--off]="s.status === 'READY' && !isSelected(s)">
|
||||||
|
@if (s.status === 'READY') {
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="nbd-source-check"
|
||||||
|
[checked]="isSelected(s)"
|
||||||
|
(change)="toggleSource(s)"
|
||||||
|
[title]="isSelected(s)
|
||||||
|
? 'Source utilisée par le chat — décocher pour l\'exclure'
|
||||||
|
: 'Source ignorée par le chat — cocher pour l\'inclure'" />
|
||||||
|
} @else {
|
||||||
<lucide-icon
|
<lucide-icon
|
||||||
[img]="s.status === 'READY' ? CheckCircle2 : (s.status === 'FAILED' ? AlertCircle : Loader)"
|
[img]="s.status === 'FAILED' ? AlertCircle : Loader"
|
||||||
[size]="14"
|
[size]="14"
|
||||||
[class.ok]="s.status === 'READY'" [class.fail]="s.status === 'FAILED'" [class.busy]="s.status === 'INDEXING'">
|
[class.fail]="s.status === 'FAILED'" [class.busy]="s.status === 'INDEXING'">
|
||||||
</lucide-icon>
|
</lucide-icon>
|
||||||
|
}
|
||||||
<div class="nbd-source-info">
|
<div class="nbd-source-info">
|
||||||
<div class="nbd-source-name">{{ s.filename }}</div>
|
<div class="nbd-source-name">{{ s.filename }}</div>
|
||||||
<div class="nbd-source-meta">
|
<div class="nbd-source-meta">
|
||||||
|
|||||||
@@ -39,12 +39,36 @@
|
|||||||
}
|
}
|
||||||
.nbd-upload-error { color: #e88; font-size: 0.8rem; margin: 0.2rem 0; }
|
.nbd-upload-error { color: #e88; font-size: 0.8rem; margin: 0.2rem 0; }
|
||||||
|
|
||||||
|
// Compteur de sources actives — ambre quand une partie est décochée, pour
|
||||||
|
// rappeler que le chat ne voit pas tout.
|
||||||
|
.nbd-sources-hint {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-text-muted, #9aa0aa);
|
||||||
|
margin: 0.1rem 0 0.2rem;
|
||||||
|
|
||||||
|
&.partial { color: #fbbf24; }
|
||||||
|
}
|
||||||
|
|
||||||
.nbd-source {
|
.nbd-source {
|
||||||
display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.5rem;
|
display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.5rem;
|
||||||
border-radius: 7px; background: rgba(255,255,255,0.03);
|
border-radius: 7px; background: rgba(255,255,255,0.03);
|
||||||
lucide-icon.ok { color: #6bd08a; }
|
lucide-icon.ok { color: #6bd08a; }
|
||||||
lucide-icon.fail { color: #e88; }
|
lucide-icon.fail { color: #e88; }
|
||||||
lucide-icon.busy { color: #e0c074; }
|
lucide-icon.busy { color: #e0c074; }
|
||||||
|
|
||||||
|
// Case « source utilisée par le chat » (sources prêtes uniquement).
|
||||||
|
.nbd-source-check {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
accent-color: #6bd08a;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source décochée : grisée = hors du périmètre du chat et de l'analyse.
|
||||||
|
&.nbd-source--off {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.nbd-source-info { flex: 1; min-width: 0; }
|
.nbd-source-info { flex: 1; min-width: 0; }
|
||||||
.nbd-source-name { font-size: 0.85rem; font-weight: 500; word-break: break-word; }
|
.nbd-source-name { font-size: 0.85rem; font-weight: 500; word-break: break-word; }
|
||||||
.nbd-source-meta { font-size: 0.72rem; color: var(--color-text-muted, #9aa0aa); }
|
.nbd-source-meta { font-size: 0.72rem; color: var(--color-text-muted, #9aa0aa); }
|
||||||
|
|||||||
@@ -137,13 +137,54 @@ export class NotebookDetailComponent implements OnInit {
|
|||||||
this.detail = d;
|
this.detail = d;
|
||||||
this.sources = d.sources ?? [];
|
this.sources = d.sources ?? [];
|
||||||
this.messages = d.messages ?? [];
|
this.messages = d.messages ?? [];
|
||||||
|
this.syncSelection();
|
||||||
},
|
},
|
||||||
error: () => this.back()
|
error: () => this.back()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadSources(): void {
|
reloadSources(): void {
|
||||||
this.service.get(this.notebookId).subscribe({ next: (d) => this.sources = d.sources ?? [] });
|
this.service.get(this.notebookId).subscribe({ next: (d) => {
|
||||||
|
this.sources = d.sources ?? [];
|
||||||
|
this.syncSelection();
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sélection des sources utilisées par le chat -------------------------
|
||||||
|
// Par défaut tout est coché ; décocher permet de limiter une question (et
|
||||||
|
// surtout une analyse approfondie, coûteuse en requêtes) à certains PDF.
|
||||||
|
|
||||||
|
/** IDs des sources cochées (sous-ensemble des sources READY). */
|
||||||
|
selectedSourceIds = new Set<string>();
|
||||||
|
/** IDs déjà vus — pour ne cocher par défaut que les NOUVELLES sources. */
|
||||||
|
private knownSourceIds = new Set<string>();
|
||||||
|
|
||||||
|
/** Aligne la sélection sur la liste courante : nouvelles sources cochées par
|
||||||
|
* défaut, sources supprimées retirées, choix de l'utilisateur préservés. */
|
||||||
|
private syncSelection(): void {
|
||||||
|
const readyIds = new Set(this.sources.filter(s => s.status === 'READY').map(s => s.id));
|
||||||
|
for (const id of readyIds) {
|
||||||
|
if (!this.knownSourceIds.has(id)) {
|
||||||
|
this.selectedSourceIds.add(id);
|
||||||
|
this.knownSourceIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of [...this.selectedSourceIds]) {
|
||||||
|
if (!readyIds.has(id)) this.selectedSourceIds.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected(s: NotebookSource): boolean {
|
||||||
|
return this.selectedSourceIds.has(s.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSource(s: NotebookSource): void {
|
||||||
|
if (this.selectedSourceIds.has(s.id)) this.selectedSourceIds.delete(s.id);
|
||||||
|
else this.selectedSourceIds.add(s.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get readySourceCount(): number {
|
||||||
|
return this.sources.filter(s => s.status === 'READY').length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Sources ---
|
// --- Sources ---
|
||||||
@@ -181,7 +222,7 @@ export class NotebookDetailComponent implements OnInit {
|
|||||||
this.messages.push(assistant);
|
this.messages.push(assistant);
|
||||||
this.sending = true;
|
this.sending = true;
|
||||||
|
|
||||||
this.service.streamChat(this.notebookId, text, deep).subscribe({
|
this.service.streamChat(this.notebookId, text, deep, [...this.selectedSourceIds]).subscribe({
|
||||||
next: (ev) => {
|
next: (ev) => {
|
||||||
if (ev.type === 'token') { this.deepProgress = null; assistant.content += ev.value; }
|
if (ev.type === 'token') { this.deepProgress = null; assistant.content += ev.value; }
|
||||||
else if (ev.type === 'sources') assistant.sources = ev.sources;
|
else if (ev.type === 'sources') assistant.sources = ev.sources;
|
||||||
|
|||||||
@@ -47,8 +47,11 @@ export class NotebookService {
|
|||||||
/**
|
/**
|
||||||
* Chat ancré streamé. fetch() + ReadableStream (HttpClient bufferise les SSE).
|
* Chat ancré streamé. fetch() + ReadableStream (HttpClient bufferise les SSE).
|
||||||
* Émissions forcées dans la zone Angular pour la détection de changement.
|
* Émissions forcées dans la zone Angular pour la détection de changement.
|
||||||
|
*
|
||||||
|
* `sourceIds` : sous-ensemble de sources à utiliser pour ce tour (cases cochées) ;
|
||||||
|
* undefined = toutes les sources prêtes du notebook.
|
||||||
*/
|
*/
|
||||||
streamChat(notebookId: string, message: string, deep = false): Observable<NotebookChatEvent> {
|
streamChat(notebookId: string, message: string, deep = false, sourceIds?: string[]): Observable<NotebookChatEvent> {
|
||||||
return new Observable<NotebookChatEvent>((subscriber) => {
|
return new Observable<NotebookChatEvent>((subscriber) => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const emit = (ev: NotebookChatEvent) => this.zone.run(() => subscriber.next(ev));
|
const emit = (ev: NotebookChatEvent) => this.zone.run(() => subscriber.next(ev));
|
||||||
@@ -59,7 +62,7 @@ export class NotebookService {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({ message, deep }),
|
body: JSON.stringify({ message, deep, sourceIds: sourceIds ?? null }),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
|
|||||||
Reference in New Issue
Block a user