Interfaccia Chat Angular RAG

Da Componenti Standalone a Dashboard Amministrativi Completi

Il frontend è dove RAG prende vita per gli utenti finali. Un backend tecnicamente perfetto ma lento da usare o confuso da navigare è praticamente inutile. Questo articolo riguarda la costruzione di un'interfaccia chat Angular intuitiva e reattiva che non sacrifica la potenza per la semplicità.

Angular moderno—componenti standalone, niente NgModules, form reattivi, Material Design 3—permette un codice snello e manutenibile. Aggiungi il supporto per l'internazionalizzazione (i18n), la visibilità delle funzionalità basata sui ruoli e le risposte in streaming, e avrai un frontend che sussurra: "Questo sistema è stato costruito da persone che ci tengono."

La Versione di Angular: Moderna, Non Legacy

Il sistema RAG utilizza Angular 20 con:

  • Componenti standalone (nessuna dichiarazione NgModule)
  • Architettura reattiva (osservabili RxJS, non promesse)
  • Angular Material 3 (stile e componenti moderni)
  • Transloco (i18n per il supporto multilingue)
  • Modalità TypeScript strict (nessun tipo any nei percorsi critici)

Questo significa:

  • Bundle più veloci (tree-shaking funziona meglio)
  • Iniezione di dipendenze più chiara (importazioni esplicite)
  • Migliori prestazioni (ottimizzazione del rilevamento delle modifiche)
  • Test più semplici (mocking minimo richiesto)

Architettura dei Componenti: Il Pattern di Composizione

Invece di un unico componente dashboard monolitico, l'interfaccia chat è costruita da pezzi piccoli, mirati e riutilizzabili.


┌─────────────────────────────────────────────┐
│         DashboardComponent                  │
│  (orchestratore, gestore dello stato, layout)│
├─────────────┬─────────────────┬─────────────┤
│             │                 │             │
▼             ▼                 ▼             ▼
ChatHistory  ChatInput       ChatMessage    ChatActions
(messaggi)   (textarea)      (renderizzati) (bottoni)
             │
             └─ SourceBadge

DashboardComponent: L'Orchestratore


@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [
    /* ... */
    ChatActionsComponent
  ],
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.scss'],
  encapsulation: ViewEncapsulation.None  // Tematizzazione Material
})
export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked {
  private destroy$ = new Subject();
  private shouldScrollToBottom = false;

  // Stato
  messages: ChatMessage[] = [];
  isLoading = false;
  currentUser: IUser | null = null;

  // Configurazione RAG
  selectedModelId: number | null = null;
  selectedContextId: number | null = null;
  topK = 5;
  rerank = false;
  formattingMode: 'default' | 'structured' = 'default';

  // Stato UI
  @ViewChild('messagesContainer') 
  private messagesContainer!: ElementRef;

  constructor(
    /* ... */
    private chatService: ChatService
  ) {}

  ngOnInit() {
    // ...
  }

  async sendMessage(question: string) {
    // ...
  }

  ngAfterViewChecked() {
    // ...
  }

  ngOnDestroy() {
    // ...
  }
}

ChatHistoryComponent: Visualizzazione dei Messaggi


@Component({
  selector: 'app-chat-history',
  standalone: true,
  imports: [CommonModule, ChatMessageComponent],
  template: `
    

  `,
  styles: [`
  `]
})
export class ChatHistoryComponent {
  @Input() messages: ChatMessage[] = [];
}

ChatInputComponent: Inserimento Domande


@Component({
  selector: 'app-chat-input',
  standalone: true,
  imports: [CommonModule, FormsModule, MatButtonModule, MatIconModule],
  template: `
    

  `,
  styles: [`
  `]
})
export class ChatInputComponent {
  @Output() submit = new EventEmitter();
  @Input() isLoading = false;

  question = '';

  onSubmit() {
    // ...
  }

  handleEnter(event: KeyboardEvent) {
    // ...
  }
}

ChatMessageComponent: Rendering con Citazioni


@Component({
  selector: 'app-chat-message',
  standalone: true,
  imports: [CommonModule, MatButtonModule, MatIconModule],
  template: `
    

  `,
  styles: [`
  `]
})
export class ChatMessageComponent {
  @Input() message!: ChatMessage;
  @Output() sourceClick = new EventEmitter();

  get isUser(): boolean {
    // ...
  }

  formatResponse(response: string): string {
    // ...
  }

  onSourceClick(ref: SourceReference) {
    // ...
  }
}

Gestione dello Stato: Pattern Store Service

Invece di NgRx (che può essere eccessivo per questo caso d'uso), il sistema RAG utilizza un pattern più semplice chiamato Store Service:


@Injectable({ providedIn: 'root' })
export class ChatStateService {
  private messagesSubject = new BehaviorSubject([]);
  private modelsSubject = new BehaviorSubject([]);
  private contextsSubject = new BehaviorSubject([]);

  messages$ = this.messagesSubject.asObservable();
  models$ = this.modelsSubject.asObservable();
  contexts$ = this.contextsSubject.asObservable();

  addMessage(message: ChatMessage) {
    // ...
  }

  loadContexts(contexts: IContext[]) {
    // ...
  }

  clearMessages() {
    // ...
  }
}

Perché non NgRx?

  • Troppo boilerplate per uno stato semplice
  • Curva di apprendimento per i nuovi membri del team
  • Complessità di debug con tracciamento azione/riduttore
  • Store Service è 80/20: 80% dei benefici di NgRx, 20% della complessità

Per applicazioni complesse e multi-funzionalità con stato condiviso tra molti componenti, NgRx ha senso. Per un'interfaccia chat concentrata, Store Service è pragmatico.

Aggiornamenti in Tempo Reale & Risposte Streaming

Quando si interroga il backend, le risposte devono sembrare istantanee anche se l'LLM impiega 5+ secondi.

Pattern di Risposta Streaming


async streamQuery(question: string): Promise {
  // Inizio con ack immediato
  const tempMessage: ChatMessage = {/* ... */};
  this.messages.push(tempMessage);

  try {
    /* ... */
    this.shouldScrollToBottom = true;
  } catch (error) {/* ... */}
}

Alternativamente, con SSE (Server-Sent Events):


streamQueryWithSSE(question: string) {
  const eventSource = new EventSource(
    /* ... */
  );

  let accumulatedResponse = '';
  const messageIndex = this.messages.length - 1;

  /* ... */
}

Switch di Contesto: Knowledge Base Multi-Dominio

Gli utenti possono passare tra diversi domini di conoscenza senza uscire dall'app.


export interface IContext {/* ... */}

UI per la selezione del contesto:



Quando il contesto cambia:

  1. Cancella la cronologia chat (o tagga i messaggi per contesto)
  2. Aggiorna selectedContextId
  3. Recupera modelli e impostazioni specifiche del contesto
  4. Resetta i parametri RAG ai default del contesto

selectContext(contextId: number) {/* ... */}

async loadContextSettings(contextId: number) {/* ... */}

Funzionalità Admin: Gestione Documenti & Contesti

Lo stesso frontend serve due audience: utenti normali (chat) e admin (gestione).

Struttura Tab Dashboard Admin





Componente Gestione Documenti


@Component({
  /* ... */
  `
})
export class DocumentManagementComponent implements OnInit {/* ... */}

Componente Gestione Contesti


@Component({
  /* ... */
  `
})
export class ContextManagementComponent implements OnInit {/* ... */}

Internazionalizzazione: Transloco per Multilingua

L'interfaccia chat supporta più lingue senza ricostruire l'app.

Setup i18n

File di traduzione (i18n/en.json):


{/* ... */}

Nei componenti:



Switch lingua:


switchLanguage(lang: string) {/* ... */}

Rendering UI Basato sui Ruoli

Non tutte le funzionalità sono per tutti. Gli admin vedono i tab di gestione, gli utenti no.


// Nel componente
get isAdmin(): boolean {

get canManageContexts(): boolean {



Gestione Errori & Feedback Utente

Gli errori devono essere informativi senza essere tecnici.


private handleError(error: any) {/* ... */}

Notifiche snackbar:


this.notificationService.success('Documento caricato con successo');
this.notificationService.error('Errore nel processare il documento');
this.notificationService.info('Documento in fase di indicizzazione...');

Ottimizzazioni Prestazionali

OnPush Change Detection


@Component({
  /* ... */
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatMessageComponent {/* ... */}

Virtual Scrolling (per cronologie chat lunghe)



  
  

Tab Lazy-Loaded

Carica i pannelli admin solo quando vengono aperti:



  
  

Material Design 3 & Tematizzazione

La UI si adatta al tema di sistema (chiaro/scuro) usando Material 3.

Configurazione tema (styles.scss):


@import '@angular/material/prebuilt-themes/indigo-pink.css';

// Tema custom (material.io theme builder)
.dark-theme {/* ... */}

.light-theme {/* ... */}

Toggle tema:


toggleTheme() {/* ... */}

Prossimi Passi

Il frontend porta il sistema RAG agli utenti. Nel prossimo articolo esploreremo il cambio provider LLM: come basta una configurazione per passare da API cloud a motori locali senza toccare il codice.

---

Key Takeaways:

Componenti standalone = bundle più puliti e snelli

Pattern composizione = pezzi piccoli, testabili, riutilizzabili

Store Service = gestione stato semplice per questa scala

Material Design 3 = UI moderna, accessibile, responsiva

Transloco = i18n senza complessità

Rendering basato sui ruoli = funzionalità diverse per utenti diversi

OnPush + Virtual scrolling = velocissimo anche con cronologie lunghe

Il frontend è uno strato sottile e bello sopra un backend solido. Gli utenti non devono pensare all'architettura—devono solo fare domande e ottenere risposte.

---

GitHub: