Compare commits
3 Commits
v0.11.0-be
...
v0.11.1-be
| Author | SHA1 | Date | |
|---|---|---|---|
| f638fdd24b | |||
| e85ab0e6b1 | |||
| ed22d9f29c |
@@ -63,11 +63,18 @@ class NotebookDeepUseCase:
|
||||
async def stream(
|
||||
self,
|
||||
source_ids: list[str],
|
||||
question: str,
|
||||
messages: list[ChatMessage],
|
||||
context: str = "",
|
||||
history_limit: int = 8,
|
||||
) -> AsyncIterator[dict]:
|
||||
"""Yield des évènements : {type:'progress',current,total}, {type:'token',token},
|
||||
{type:'done'}. (Les erreurs LLM des lots sont tolérées : lot ignoré.)"""
|
||||
{type:'done'}. (Les erreurs LLM des lots sont tolérées : lot ignoré.)
|
||||
|
||||
La dernière question utilisateur sert à la LECTURE du document (map) ; la
|
||||
SYNTHÈSE (reduce) reçoit les `history_limit` derniers messages → les relances
|
||||
conversationnelles (« et pour les autres ? ») fonctionnent aussi en approfondi.
|
||||
"""
|
||||
question = next((m.content for m in reversed(messages) if m.role == "user"), "")
|
||||
chunks: list[dict] = []
|
||||
for sid in source_ids:
|
||||
chunks.extend(vector_store.all_chunks(sid))
|
||||
@@ -102,10 +109,11 @@ class NotebookDeepUseCase:
|
||||
if context.strip() else ""
|
||||
)
|
||||
system_prompt = _REDUCE_SYSTEM.format(context_block=context_block, notes_block=notes_block)
|
||||
# Historique récent pour la cohérence des relances ; on garantit que le
|
||||
# dernier message est bien la question courante.
|
||||
reduce_messages = messages[-history_limit:] if messages else [ChatMessage(role="user", content=question)]
|
||||
llm_chat: LLMChatProvider = self._llm # type: ignore[assignment]
|
||||
async for token in llm_chat.stream_chat(
|
||||
[ChatMessage(role="user", content=question)], system_prompt=system_prompt
|
||||
):
|
||||
async for token in llm_chat.stream_chat(reduce_messages, system_prompt=system_prompt):
|
||||
yield {"type": "token", "token": token}
|
||||
yield {"type": "done"}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ from app.infrastructure.pdf_extractor import PyMuPdfTextExtractor
|
||||
app = FastAPI(
|
||||
title="LoreMind Brain",
|
||||
description="Backend IA pour la génération de contenu narratif.",
|
||||
version="0.11.0-beta",
|
||||
version="0.11.1-beta",
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1100,7 +1100,8 @@ async def chat_notebook_deep_stream(
|
||||
) -> StreamingResponse:
|
||||
"""Analyse APPROFONDIE (map-reduce sur tout le document). Évènements SSE :
|
||||
`progress` {current,total} pendant la lecture, puis `token` {token}, puis `done`."""
|
||||
question = next((m.content for m in reversed(body.messages) if m.role == "user"), "")
|
||||
messages = [ChatMessage(role=m.role, content=m.content) for m in body.messages]
|
||||
question = next((m.content for m in reversed(messages) if m.role == "user"), "")
|
||||
|
||||
def _sse(event: str, data: dict) -> str:
|
||||
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||
@@ -1110,7 +1111,7 @@ async def chat_notebook_deep_stream(
|
||||
yield _sse("error", {"message": "Question vide."})
|
||||
return
|
||||
try:
|
||||
async for ev in use_case.stream(body.source_ids, question, context=body.context):
|
||||
async for ev in use_case.stream(body.source_ids, messages, context=body.context):
|
||||
ev_type = ev.pop("type")
|
||||
yield _sse(ev_type, ev)
|
||||
except (LLMProviderError, EmbeddingError) as exc:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<groupId>com.loremind</groupId>
|
||||
<artifactId>loremind-core</artifactId>
|
||||
<version>0.11.0-beta</version>
|
||||
<version>0.11.1-beta</version>
|
||||
<name>LoreMind Core</name>
|
||||
<description>Backend Core - Architecture Hexagonale</description>
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ public class NpcService {
|
||||
Map<String, List<String>> imageValues,
|
||||
Map<String, Map<String, String>> keyValueValues,
|
||||
String campaignId,
|
||||
String folder,
|
||||
Integer order
|
||||
) {}
|
||||
|
||||
@@ -44,6 +45,7 @@ public class NpcService {
|
||||
.imageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>())
|
||||
.keyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>())
|
||||
.campaignId(data.campaignId())
|
||||
.folder(normalizeFolder(data.folder()))
|
||||
.order(order)
|
||||
.build();
|
||||
return npcRepository.save(npc);
|
||||
@@ -66,6 +68,7 @@ public class NpcService {
|
||||
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
||||
existing.setImageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>());
|
||||
existing.setKeyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>());
|
||||
existing.setFolder(normalizeFolder(data.folder()));
|
||||
if (data.order() != null) {
|
||||
existing.setOrder(data.order());
|
||||
}
|
||||
@@ -76,6 +79,13 @@ public class NpcService {
|
||||
npcRepository.deleteById(id);
|
||||
}
|
||||
|
||||
/** Trim le dossier ; chaîne vide → null (= non classé). */
|
||||
private static String normalizeFolder(String folder) {
|
||||
if (folder == null) return null;
|
||||
String trimmed = folder.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private int nextOrderFor(String campaignId) {
|
||||
return npcRepository.findByCampaignId(campaignId).stream()
|
||||
.mapToInt(Npc::getOrder)
|
||||
|
||||
@@ -46,6 +46,9 @@ public class Npc {
|
||||
/** Référence vers la Campaign parente (cross-aggregate via ID). */
|
||||
private String campaignId;
|
||||
|
||||
/** Dossier de classement (texte libre, ex. « Bard's Gate »). Nullable = non classé. */
|
||||
private String folder;
|
||||
|
||||
/** Ordre d'affichage dans la liste des PNJ de la campagne. */
|
||||
private int order;
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ public class NpcJpaEntity {
|
||||
@Column(name = "campaign_id", nullable = false)
|
||||
private Long campaignId;
|
||||
|
||||
@Column(name = "folder")
|
||||
private String folder;
|
||||
|
||||
@Column(name = "\"order\"", nullable = false)
|
||||
private int order;
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ public class PostgresNpcRepository implements NpcRepository {
|
||||
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(e.getKeyValueValues() != null ? new HashMap<>(e.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(e.getCampaignId().toString())
|
||||
.folder(e.getFolder())
|
||||
.order(e.getOrder())
|
||||
.createdAt(e.getCreatedAt())
|
||||
.updatedAt(e.getUpdatedAt())
|
||||
@@ -76,6 +77,7 @@ public class PostgresNpcRepository implements NpcRepository {
|
||||
.imageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(Long.parseLong(n.getCampaignId()))
|
||||
.folder(n.getFolder())
|
||||
.order(n.getOrder())
|
||||
.createdAt(n.getCreatedAt())
|
||||
.updatedAt(n.getUpdatedAt())
|
||||
|
||||
@@ -64,6 +64,7 @@ public class NpcController {
|
||||
dto.getImageValues(),
|
||||
dto.getKeyValueValues(),
|
||||
dto.getCampaignId(),
|
||||
dto.getFolder(),
|
||||
order
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@ public class NpcDTO {
|
||||
private Map<String, List<String>> imageValues = new HashMap<>();
|
||||
private Map<String, Map<String, String>> keyValueValues = new HashMap<>();
|
||||
private String campaignId;
|
||||
private String folder;
|
||||
private int order;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public class NpcMapper {
|
||||
dto.setImageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : new HashMap<>());
|
||||
dto.setKeyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>());
|
||||
dto.setCampaignId(n.getCampaignId());
|
||||
dto.setFolder(n.getFolder());
|
||||
dto.setOrder(n.getOrder());
|
||||
return dto;
|
||||
}
|
||||
@@ -35,6 +36,7 @@ public class NpcMapper {
|
||||
.imageValues(dto.getImageValues() != null ? new HashMap<>(dto.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(dto.getKeyValueValues() != null ? new HashMap<>(dto.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(dto.getCampaignId())
|
||||
.folder(dto.getFolder())
|
||||
.order(dto.getOrder())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class NpcServiceTest {
|
||||
|
||||
Npc result = npcService.createNpc(
|
||||
new NpcService.NpcData("Borin le forgeron", null, null,
|
||||
Map.of("Notes", "Borin"), null, null, "camp-1", 5));
|
||||
Map.of("Notes", "Borin"), null, null, "camp-1", null,5));
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||
@@ -67,7 +67,7 @@ public class NpcServiceTest {
|
||||
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of(a, b));
|
||||
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
|
||||
|
||||
npcService.createNpc(new NpcService.NpcData("Nouveau", null, null, null, null, null, "camp-1", null));
|
||||
npcService.createNpc(new NpcService.NpcData("Nouveau", null, null, null, null, null, "camp-1", null,null));
|
||||
|
||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||
verify(npcRepository).save(captor.capture());
|
||||
@@ -79,7 +79,7 @@ public class NpcServiceTest {
|
||||
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of());
|
||||
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
|
||||
|
||||
npcService.createNpc(new NpcService.NpcData("Premier", null, null, null, null, null, "camp-1", null));
|
||||
npcService.createNpc(new NpcService.NpcData("Premier", null, null, null, null, null, "camp-1", null,null));
|
||||
|
||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||
verify(npcRepository).save(captor.capture());
|
||||
@@ -124,7 +124,7 @@ public class NpcServiceTest {
|
||||
|
||||
Npc result = npcService.updateNpc("npc-1",
|
||||
new NpcService.NpcData("Borin renommé", null, null,
|
||||
Map.of("Notes", "v2"), null, null, "camp-1", 7));
|
||||
Map.of("Notes", "v2"), null, null, "camp-1", null,7));
|
||||
|
||||
assertEquals("Borin renommé", result.getName());
|
||||
assertEquals("v2", result.getValues().get("Notes"));
|
||||
@@ -138,7 +138,7 @@ public class NpcServiceTest {
|
||||
|
||||
Npc result = npcService.updateNpc("npc-1",
|
||||
new NpcService.NpcData("Borin", null, null,
|
||||
Map.of("Notes", "txt"), null, null, "camp-1", null));
|
||||
Map.of("Notes", "txt"), null, null, "camp-1", null,null));
|
||||
|
||||
// testNpc avait order=1 → préservé
|
||||
assertEquals(1, result.getOrder());
|
||||
@@ -150,7 +150,7 @@ public class NpcServiceTest {
|
||||
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> npcService.updateNpc("missing",
|
||||
new NpcService.NpcData("x", null, null, null, null, null, "camp-1", null)));
|
||||
new NpcService.NpcData("x", null, null, null, null, null, "camp-1", null,null)));
|
||||
assertTrue(ex.getMessage().contains("missing"));
|
||||
verify(npcRepository, never()).save(any());
|
||||
}
|
||||
|
||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "loremind-web",
|
||||
"version": "0.11.0-beta",
|
||||
"version": "0.11.1-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "loremind-web",
|
||||
"version": "0.11.0-beta",
|
||||
"version": "0.11.1-beta",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "loremind-web",
|
||||
"version": "0.11.0-beta",
|
||||
"version": "0.11.1-beta",
|
||||
"description": "LoreMind Frontend - Angular",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
|
||||
@@ -96,18 +96,45 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T
|
||||
// à une Partie (Playthrough). On ne les affiche donc plus dans la sidebar de
|
||||
// campagne — seuls les PNJ (donnée de scénario) restent sous "Personnages".
|
||||
const sortedNpcs = [...data.npcs].sort(byName);
|
||||
const npcItems: TreeItem[] = sortedNpcs.map(n => ({
|
||||
const npcItem = (n: Npc): TreeItem => ({
|
||||
id: `npc-${n.id}`,
|
||||
label: n.name,
|
||||
route: `/campaigns/${campaignId}/npcs/${n.id}`
|
||||
}));
|
||||
});
|
||||
|
||||
// Regroupement par DOSSIER : un sous-nœud (dépliable) par dossier, puis les PNJ
|
||||
// non classés directement sous « PNJ ».
|
||||
const npcsByFolder = new Map<string, Npc[]>();
|
||||
const ungroupedNpcs: Npc[] = [];
|
||||
for (const n of sortedNpcs) {
|
||||
const f = (n.folder ?? '').trim();
|
||||
if (f) {
|
||||
if (!npcsByFolder.has(f)) npcsByFolder.set(f, []);
|
||||
npcsByFolder.get(f)!.push(n);
|
||||
} else {
|
||||
ungroupedNpcs.push(n);
|
||||
}
|
||||
}
|
||||
const npcFolderNodes: TreeItem[] = [...npcsByFolder.keys()]
|
||||
.sort((a, b) => a.localeCompare(b, 'fr', { sensitivity: 'base' }))
|
||||
.map(folder => {
|
||||
const items = npcsByFolder.get(folder)!.map(npcItem);
|
||||
return {
|
||||
id: `npc-folder-${folder}`,
|
||||
label: folder,
|
||||
iconKey: 'folder',
|
||||
children: items,
|
||||
meta: String(items.length)
|
||||
};
|
||||
});
|
||||
const npcChildren: TreeItem[] = [...npcFolderNodes, ...ungroupedNpcs.map(npcItem)];
|
||||
|
||||
const npcsNode: TreeItem = {
|
||||
id: 'npcs-root',
|
||||
label: 'PNJ',
|
||||
iconKey: 'c-drama',
|
||||
children: npcItems,
|
||||
meta: npcItems.length ? String(npcItems.length) : undefined,
|
||||
children: npcChildren,
|
||||
meta: sortedNpcs.length ? String(sortedNpcs.length) : undefined,
|
||||
// Porte le header de section "Personnages" (les PJ ayant migré vers la Partie).
|
||||
// Le filet au-dessus est masqué par CSS si c'est le tout premier item de la sidebar.
|
||||
sectionHeaderBefore: 'Personnages',
|
||||
|
||||
@@ -36,6 +36,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="npc-folder">Dossier</label>
|
||||
<input
|
||||
id="npc-folder"
|
||||
type="text"
|
||||
[(ngModel)]="folder"
|
||||
name="folder"
|
||||
list="npc-folders"
|
||||
placeholder="Ex: Bard's Gate, Faction des Pipers… (laisser vide = non classé)"
|
||||
/>
|
||||
<datalist id="npc-folders">
|
||||
<option *ngFor="let f of existingFolders" [value]="f"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div class="field-row image-row">
|
||||
<div class="field portrait-field">
|
||||
<label>Portrait</label>
|
||||
|
||||
@@ -46,6 +46,9 @@ export class NpcEditComponent implements OnInit {
|
||||
npcId: string | null = null;
|
||||
|
||||
name = '';
|
||||
folder = '';
|
||||
/** Dossiers déjà utilisés dans la campagne (datalist d'auto-complétion). */
|
||||
existingFolders: string[] = [];
|
||||
portraitImageId: string | null = null;
|
||||
headerImageId: string | null = null;
|
||||
values: Record<string, string> = {};
|
||||
@@ -72,12 +75,14 @@ export class NpcEditComponent implements OnInit {
|
||||
if (this.campaignId) {
|
||||
this.loadTemplateForCampaign(this.campaignId);
|
||||
this.campaignSidebar.show(this.campaignId);
|
||||
this.loadExistingFolders(this.campaignId);
|
||||
}
|
||||
|
||||
if (this.npcId) {
|
||||
this.service.getById(this.npcId).subscribe({
|
||||
next: (n) => {
|
||||
this.name = n.name;
|
||||
this.folder = n.folder ?? '';
|
||||
this.portraitImageId = n.portraitImageId ?? null;
|
||||
this.headerImageId = n.headerImageId ?? null;
|
||||
this.values = n.values ?? {};
|
||||
@@ -90,6 +95,17 @@ export class NpcEditComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private loadExistingFolders(campaignId: string): void {
|
||||
this.service.getByCampaign(campaignId).subscribe({
|
||||
next: (list) => {
|
||||
this.existingFolders = [...new Set(
|
||||
list.map(n => (n.folder ?? '').trim()).filter(f => f.length > 0)
|
||||
)].sort((a, b) => a.localeCompare(b, 'fr'));
|
||||
},
|
||||
error: () => { this.existingFolders = []; }
|
||||
});
|
||||
}
|
||||
|
||||
private loadTemplateForCampaign(campaignId: string): void {
|
||||
this.campaignService.getCampaignById(campaignId).subscribe({
|
||||
next: (campaign) => {
|
||||
@@ -111,6 +127,7 @@ export class NpcEditComponent implements OnInit {
|
||||
if (!this.name.trim() || !this.campaignId) return;
|
||||
const payload = {
|
||||
name: this.name.trim(),
|
||||
folder: this.folder.trim() || null,
|
||||
portraitImageId: this.portraitImageId,
|
||||
headerImageId: this.headerImageId,
|
||||
values: this.values,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { LucideAngularModule, ArrowLeft, Edit3, Sparkles } from 'lucide-angular';
|
||||
import { NpcService } from '../../../services/npc.service';
|
||||
import { CampaignService } from '../../../services/campaign.service';
|
||||
@@ -22,7 +23,7 @@ import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-dr
|
||||
templateUrl: './npc-view.component.html',
|
||||
styleUrls: ['./npc-view.component.scss']
|
||||
})
|
||||
export class NpcViewComponent implements OnInit {
|
||||
export class NpcViewComponent implements OnInit, OnDestroy {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Edit3 = Edit3;
|
||||
readonly Sparkles = Sparkles;
|
||||
@@ -36,6 +37,8 @@ export class NpcViewComponent implements OnInit {
|
||||
chatOpen = false;
|
||||
toggleChat(): void { this.chatOpen = !this.chatOpen; }
|
||||
|
||||
private paramsSub?: Subscription;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
@@ -46,25 +49,42 @@ export class NpcViewComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const params = this.route.snapshot.paramMap;
|
||||
this.campaignId = params.get('campaignId');
|
||||
this.npcId = params.get('npcId');
|
||||
if (this.npcId) {
|
||||
this.service.getById(this.npcId).subscribe({
|
||||
next: n => { this.npc = n; },
|
||||
error: () => this.back()
|
||||
});
|
||||
}
|
||||
if (this.campaignId) {
|
||||
this.campaignSidebar.show(this.campaignId);
|
||||
this.campaignService.getCampaignById(this.campaignId).subscribe(camp => {
|
||||
if (camp.gameSystemId) {
|
||||
this.gameSystemService.getById(camp.gameSystemId).subscribe(gs => {
|
||||
this.templateFields = gs.npcTemplate ?? [];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// S'abonner aux paramMap (pas le snapshot) : quand on passe d'un PNJ à un autre,
|
||||
// Angular RÉUTILISE le composant (même route) → ngOnInit ne re-tourne pas. Sans ce
|
||||
// subscribe, la fiche du centre resterait figée sur l'ancien PNJ.
|
||||
this.paramsSub = this.route.paramMap.subscribe(params => {
|
||||
const newCampaignId = params.get('campaignId');
|
||||
this.npcId = params.get('npcId');
|
||||
|
||||
// Recharge la fiche à CHAQUE changement de PNJ.
|
||||
this.chatOpen = false;
|
||||
if (this.npcId) {
|
||||
this.service.getById(this.npcId).subscribe({
|
||||
next: n => { this.npc = n; },
|
||||
error: () => this.back()
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar + template du système : seulement quand la campagne change (inutile
|
||||
// de les recharger à chaque switch de PNJ d'une même campagne).
|
||||
if (newCampaignId && newCampaignId !== this.campaignId) {
|
||||
this.campaignId = newCampaignId;
|
||||
this.campaignSidebar.show(this.campaignId);
|
||||
this.campaignService.getCampaignById(this.campaignId).subscribe(camp => {
|
||||
if (camp.gameSystemId) {
|
||||
this.gameSystemService.getById(camp.gameSystemId).subscribe(gs => {
|
||||
this.templateFields = gs.npcTemplate ?? [];
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (newCampaignId) {
|
||||
this.campaignId = newCampaignId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paramsSub?.unsubscribe();
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface Npc {
|
||||
imageValues?: Record<string, string[]>;
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
campaignId: string;
|
||||
/** Dossier de classement (ex. « Bard's Gate »). Vide/absent = non classé. */
|
||||
folder?: string | null;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
@@ -22,4 +24,5 @@ export interface NpcCreate {
|
||||
imageValues?: Record<string, string[]>;
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
campaignId: string;
|
||||
folder?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user