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:
@@ -0,0 +1,51 @@
|
||||
<div class="srt-panel">
|
||||
<p class="loading-hint" *ngIf="loading">Chargement…</p>
|
||||
|
||||
<!-- Liste des tables -->
|
||||
<div *ngIf="!loading && !selected" class="srt-list">
|
||||
<p class="empty-hint" *ngIf="tables.length === 0">
|
||||
Aucune table aléatoire dans cette campagne. Crée-en une depuis la sidebar.
|
||||
</p>
|
||||
<button *ngFor="let t of tables" type="button" class="srt-item" (click)="select(t)">
|
||||
<lucide-icon [img]="Dices" [size]="13"></lucide-icon>
|
||||
<span class="srt-item-name">{{ t.name }}</span>
|
||||
<span class="srt-item-formula">{{ t.diceFormula }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Table sélectionnée : jet -->
|
||||
<div *ngIf="selected" class="srt-detail">
|
||||
<button type="button" class="srt-back" (click)="backToList()">
|
||||
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Tables
|
||||
</button>
|
||||
|
||||
<h4>{{ selected.name }}</h4>
|
||||
|
||||
<button type="button" class="srt-roll" (click)="roll()">
|
||||
<lucide-icon [img]="Dices" [size]="15"></lucide-icon>
|
||||
Lancer {{ selected.diceFormula }}
|
||||
</button>
|
||||
|
||||
<div class="srt-result" *ngIf="lastRoll">
|
||||
<div class="srt-total">
|
||||
{{ lastRoll.total }}
|
||||
<span class="srt-rolls" *ngIf="lastRoll.rolls.length > 1">({{ lastRoll.rolls.join(' + ') }})</span>
|
||||
→
|
||||
<strong *ngIf="matched">{{ matched.label }}</strong>
|
||||
<em *ngIf="!matched">aucune entrée</em>
|
||||
</div>
|
||||
<div class="srt-detail-text" *ngIf="matched?.detail">{{ matched?.detail }}</div>
|
||||
|
||||
<div class="srt-actions">
|
||||
<button type="button" class="srt-act" (click)="addToJournal()" [disabled]="!canAddToJournal">
|
||||
<lucide-icon [img]="BookmarkPlus" [size]="13"></lucide-icon> Journal
|
||||
</button>
|
||||
<button type="button" class="srt-act srt-act--ai" (click)="improvise()"
|
||||
[disabled]="!matched || improvising || !canAddToJournal">
|
||||
<lucide-icon [img]="Sparkles" [size]="13"></lucide-icon>
|
||||
{{ improvising ? '…' : 'IA improvise' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,108 @@
|
||||
.srt-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.loading-hint, .empty-hint {
|
||||
color: var(--color-text-muted, #9aa0aa);
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.srt-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
|
||||
.srt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 7px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:hover { background: rgba(255, 255, 255, 0.08); }
|
||||
|
||||
.srt-item-name { flex: 1; }
|
||||
.srt-item-formula {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted, #9aa0aa);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
padding: 0.05rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.srt-detail { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
|
||||
.srt-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
align-self: flex-start;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text-muted, #9aa0aa);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
&:hover { color: inherit; }
|
||||
}
|
||||
|
||||
h4 { margin: 0; font-size: 0.95rem; }
|
||||
|
||||
.srt-roll {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
&:hover { background: #5568d3; }
|
||||
}
|
||||
|
||||
.srt-result {
|
||||
padding: 0.6rem 0.7rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 1px solid rgba(102, 126, 234, 0.25);
|
||||
|
||||
.srt-total { font-size: 1rem; }
|
||||
.srt-rolls { color: var(--color-text-muted, #9aa0aa); font-size: 0.8rem; }
|
||||
.srt-detail-text {
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted, #cfd3da);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.srt-actions { display: flex; gap: 0.4rem; margin-top: 0.6rem; }
|
||||
.srt-act {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
&:hover:not(:disabled) { background: rgba(255, 255, 255, 0.1); }
|
||||
&:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
&--ai { border-color: rgba(168, 130, 255, 0.35); color: #c4a8ff; }
|
||||
&--ai:hover:not(:disabled) { background: rgba(168, 130, 255, 0.14); }
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, Dices, BookmarkPlus, Sparkles, ChevronLeft } from 'lucide-angular';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { RandomTableService } from '../../services/random-table.service';
|
||||
import { RandomTable, RandomTableEntry } from '../../services/random-table.model';
|
||||
import { DiceUtils, DiceRoll } from '../../shared/dice.utils';
|
||||
import { DiceRollResult } from '../session-dice-panel/session-dice-panel.component';
|
||||
|
||||
/**
|
||||
* Panneau "Tables aléatoires" du mode jeu : choisir une table de la campagne,
|
||||
* la JETER, consigner le résultat au journal, et (option) demander à l'IA
|
||||
* d'improviser un court récit sur le tirage.
|
||||
*
|
||||
* Réutilise les sorties existantes du panneau de référence :
|
||||
* - `rolled` (DiceRollResult) → entrée DICE_ROLL dans le journal ;
|
||||
* - `aiReplyToJournal` (string) → entrée NOTE (récit IA).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-session-random-tables-panel',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
templateUrl: './session-random-tables-panel.component.html',
|
||||
styleUrls: ['./session-random-tables-panel.component.scss']
|
||||
})
|
||||
export class SessionRandomTablesPanelComponent implements OnInit {
|
||||
readonly Dices = Dices;
|
||||
readonly BookmarkPlus = BookmarkPlus;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly ChevronLeft = ChevronLeft;
|
||||
|
||||
@Input() campaignId!: string;
|
||||
@Input() canAddToJournal = true;
|
||||
@Output() rolled = new EventEmitter<DiceRollResult>();
|
||||
@Output() aiReplyToJournal = new EventEmitter<string>();
|
||||
|
||||
tables: RandomTable[] = [];
|
||||
loading = false;
|
||||
|
||||
selected: RandomTable | null = null;
|
||||
lastRoll: DiceRoll | null = null;
|
||||
matched: RandomTableEntry | null = null;
|
||||
improvising = false;
|
||||
|
||||
constructor(private service: RandomTableService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.campaignId) return;
|
||||
this.loading = true;
|
||||
this.service.getByCampaign(this.campaignId).pipe(catchError(() => of([] as RandomTable[])))
|
||||
.subscribe(list => { this.tables = list; this.loading = false; });
|
||||
}
|
||||
|
||||
select(table: RandomTable): void {
|
||||
this.selected = table;
|
||||
this.lastRoll = null;
|
||||
this.matched = null;
|
||||
}
|
||||
|
||||
backToList(): void {
|
||||
this.selected = null;
|
||||
this.lastRoll = null;
|
||||
this.matched = null;
|
||||
}
|
||||
|
||||
roll(): void {
|
||||
if (!this.selected) return;
|
||||
const result = DiceUtils.roll(this.selected.diceFormula);
|
||||
if (!result) { this.lastRoll = null; this.matched = null; return; }
|
||||
this.lastRoll = result;
|
||||
this.matched = this.selected.entries.find(
|
||||
e => result.total >= e.minRoll && result.total <= e.maxRoll
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
rangeLabel(entry: RandomTableEntry): string {
|
||||
return entry.minRoll === entry.maxRoll ? String(entry.minRoll) : `${entry.minRoll}–${entry.maxRoll}`;
|
||||
}
|
||||
|
||||
/** Consigne le tirage au journal (entrée DICE_ROLL via la sortie `rolled`). */
|
||||
addToJournal(): void {
|
||||
if (!this.canAddToJournal || !this.selected || !this.lastRoll) return;
|
||||
const label = this.matched?.label ?? 'aucun résultat';
|
||||
const result: DiceRollResult = {
|
||||
notation: this.selected.diceFormula,
|
||||
rolls: this.lastRoll.rolls,
|
||||
modifier: 0,
|
||||
total: this.lastRoll.total,
|
||||
summary: `🎲 ${this.selected.name} : ${this.lastRoll.total} → ${label}`
|
||||
};
|
||||
this.rolled.emit(result);
|
||||
}
|
||||
|
||||
/** Demande à l'IA un court récit sur le résultat, puis l'envoie au journal (NOTE). */
|
||||
improvise(): void {
|
||||
if (!this.selected || !this.matched || this.improvising) return;
|
||||
this.improvising = true;
|
||||
this.service.improvise(
|
||||
this.campaignId, this.selected.name, this.matched.label, this.matched.detail ?? ''
|
||||
).subscribe({
|
||||
next: (r) => {
|
||||
this.improvising = false;
|
||||
if (r.narration?.trim() && this.canAddToJournal) {
|
||||
this.aiReplyToJournal.emit(r.narration.trim());
|
||||
}
|
||||
},
|
||||
error: () => { this.improvising = false; }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@
|
||||
<lucide-icon [img]="Dices" [size]="14"></lucide-icon>
|
||||
Dés
|
||||
</button>
|
||||
<button type="button"
|
||||
class="ref-tab"
|
||||
[class.ref-tab--active]="activeTab === 'tables'"
|
||||
(click)="selectTab('tables')">
|
||||
<lucide-icon [img]="Table2" [size]="14"></lucide-icon>
|
||||
Tables
|
||||
</button>
|
||||
<button type="button"
|
||||
class="ref-tab"
|
||||
[class.ref-tab--active]="activeTab === 'characters'"
|
||||
@@ -48,6 +55,15 @@
|
||||
(rolled)="onDiceRolled($event)">
|
||||
</app-session-dice-panel>
|
||||
|
||||
<!-- ====== Tables aléatoires ====== -->
|
||||
<app-session-random-tables-panel
|
||||
*ngIf="activeTab === 'tables'"
|
||||
[campaignId]="campaignId"
|
||||
[canAddToJournal]="canAddToJournal"
|
||||
(rolled)="onDiceRolled($event)"
|
||||
(aiReplyToJournal)="onAiSaveToJournal($event)">
|
||||
</app-session-random-tables-panel>
|
||||
|
||||
<!-- ====== Personnages (PJ + PNJ) ====== -->
|
||||
<div *ngIf="activeTab === 'characters'" class="ref-list">
|
||||
<p class="loading-hint" *ngIf="loadingChars">Chargement…</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles } from 'lucide-angular';
|
||||
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2 } from 'lucide-angular';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { CampaignService } from '../../services/campaign.service';
|
||||
import { CharacterService } from '../../services/character.service';
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
SessionDicePanelComponent, DiceRollResult
|
||||
} from '../session-dice-panel/session-dice-panel.component';
|
||||
import { SessionAiChatPanelComponent } from '../session-ai-chat-panel/session-ai-chat-panel.component';
|
||||
import { SessionRandomTablesPanelComponent } from '../session-random-tables-panel/session-random-tables-panel.component';
|
||||
|
||||
type TabId = 'dice' | 'characters' | 'scenes' | 'ai';
|
||||
type TabId = 'dice' | 'tables' | 'characters' | 'scenes' | 'ai';
|
||||
|
||||
/**
|
||||
* Panneau latéral du mode jeu : référence rapide en lecture seule.
|
||||
@@ -29,7 +30,7 @@ type TabId = 'dice' | 'characters' | 'scenes' | 'ai';
|
||||
@Component({
|
||||
selector: 'app-session-reference-panel',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent],
|
||||
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent],
|
||||
templateUrl: './session-reference-panel.component.html',
|
||||
styleUrls: ['./session-reference-panel.component.scss']
|
||||
})
|
||||
@@ -40,6 +41,7 @@ export class SessionReferencePanelComponent implements OnChanges {
|
||||
readonly Dices = Dices;
|
||||
readonly ExternalLink = ExternalLink;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly Table2 = Table2;
|
||||
|
||||
@Input() campaignId!: string;
|
||||
/** Partie active — nécessaire pour charger les PJ (refonte Playthrough). */
|
||||
@@ -109,7 +111,7 @@ export class SessionReferencePanelComponent implements OnChanges {
|
||||
if (this.treeLoaded || this.loadingTree || !this.campaignId) return;
|
||||
this.loadingTree = true;
|
||||
loadCampaignTreeData(this.campaignService, this.campaignId, this.characterService, this.npcService).pipe(
|
||||
catchError(() => of({ arcs: [], chaptersByArc: {}, scenesByChapter: {}, characters: [], npcs: [] } as CampaignTreeData))
|
||||
catchError(() => of({ arcs: [], chaptersByArc: {}, scenesByChapter: {}, characters: [], npcs: [], randomTables: [] } as CampaignTreeData))
|
||||
).subscribe(data => {
|
||||
this.treeData = data;
|
||||
this.loadingTree = false;
|
||||
|
||||
Reference in New Issue
Block a user