Nous allons construire une application complète appelée Hermes Local Assistant, avec une interface de discussion semblable à ChatGPT, mais qui fonctionne sur ton ordinateur, sans API payante.
Niveau : Intermédiaire
Cette application nous permettra de :
- Discuter avec une IA Hermes exécutée localement grâce à Ollama.
- Envoyer des questions depuis Angular.
- Recevoir les réponses générées par Hermes.
- Enregistrer les conversations dans MySQL.
- Afficher l’historique des conversations.
- Supprimer une conversation.
N.B. : Nous allons utiliser un modèle de la famille « Nous Hermes » compatible avec Ollama, et non l’agent autonome Hermes Agent, qui est un projet différent.
Préparation de l’environnement de travail
Vérifie les installations sur votre ordinateur :
node -v
npm -v
ng version
nest --version
ollama --versionSi Angular CLI et NestJS CLI ne sont pas installés :
npm install -g @angular/cli
npm install -g @nestjs/cliPour Ollama, vous pouvez utiliser le site Officiel : Installer Ollama
Installer Hermes dans Ollama
Ollama est le programme qui exécute le modèle. Hermes est le modèle qui répond aux questions.
Nous allons utiliser nous-hermes2, un modèle plus ancien, mais pratique pour apprendre les bases de l’intégration locale.
Dans ton terminal :
ollama pull nous-hermes2Cette commande va télécharger le modèle sur ton ordinateur.
Teste ensuite :
ollama run nous-hermes2Vous pouvez lui poser une question :
Explique-moi simplement ce qu'est Angular.Si le modèle répond, la première partie fonctionne.
Si vous souhaitez verifier les modèles disponibles :
ollama listPour vérifier l’API locale :
curl.exe http://localhost:11434/api/tagsOllama utilise le port 11434.
Important : nous-hermes2 est ici un choix pédagogique. Pour un modèle Hermes plus récent, vous pourrez remplacer son nom dans la configuration NestJS, à condition qu’il soit disponible dans votre installation Ollama.
Création de la base de données MySQL
Nous allons laisser TypeORM créer automatiquement les tables pour ce premier exercice.
Notre structure sera :
hermes_local
│
├── conversations
│ ├── id
│ ├── titre
│ └── createdAt
│
└── messages
├── id
├── conversationId
├── role
├── contenu
└── createdAt
Une conversation contient plusieurs messages. Le champ role permet de savoir si un message vient de l’utilisateur (user) ou de l’IA (assistant).
Création du projet NestJS
mkdir hermes-local
cd hermes-local
nest new backendJ’utilise npm comme gestionnaire de paquet
Ensuite :
cd backend
npm install @nestjs/typeorm typeorm mysql2
npm install @nestjs/config
npm install class-validator class-transformerPetite explication :
| Package | Utilité |
|---|---|
@nestjs/typeorm | Relie NestJS à TypeORM |
typeorm | Manipule les tables avec des classes |
mysql2 | Permet la connexion MySQL |
@nestjs/config | Lit le fichier .env |
class-validator | Vérifie les données reçues |
class-transformer | Transforme les données pour la validation |
Nous allons créer nos modules :
nest g module conversations
nest g controller conversations
nest g service conversations
nest g module ollama
nest g service ollamaLe résultat final :
backend/
├── .env
└── src/
├── main.ts
├── app.module.ts
│
├── conversations/
│ ├── conversations.module.ts
│ ├── conversations.controller.ts
│ ├── conversations.service.ts
│ ├── dto/
│ │ └── send-message.dto.ts
│ └── entities/
│ ├── conversation.entity.ts
│ └── message.entity.ts
│
└── ollama/
├── ollama.module.ts
└── ollama.service.tsLa configuration MySQL et Ollama
Dans backend/.env :
# Base de données locale
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=hermes_app
DB_PASSWORD=ChangeMoi_MotDePasseFort_2026!
DB_NAME=hermes_local
# Ollama local
OLLAMA_URL=http://127.0.0.1:11434
OLLAMA_MODEL=nous-hermes2
# Serveur NestJS
PORT=3000Ne publie jamais ton fichier .env sur GitHub. Vérifie qu’il figure dans .gitignore.
Le fichier src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConversationsModule } from './conversations/conversations.module';
import { OllamaModule } from './ollama/ollama.module';
@Module({
imports: [
// Rend les variables .env accessibles
ConfigModule.forRoot({
isGlobal: true,
}),
// Connexion à MySQL
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'mysql',
host: config.getOrThrow<string>('DB_HOST'),
port: Number(config.get('DB_PORT', 3306)),
username: config.getOrThrow<string>('DB_USER'),
password: config.getOrThrow<string>('DB_PASSWORD'),
database: config.getOrThrow<string>('DB_NAME'),
// Détecte automatiquement nos entités
autoLoadEntities: true,
// Uniquement pour le développement local
synchronize: true,
}),
}),
OllamaModule,
ConversationsModule,
],
})
export class AppModule {}N.B : TypeORM crée et met à jour les tables à partir de nos classes. C’est pratique pour apprendre, mais il faut utiliser des migrations en production, car la synchronisation automatique peut provoquer une perte de données.
Création de l’entités TypeORM
Dans le fichier conversations/entities/conversation.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany,
} from 'typeorm';
import { Message } from './message.entity';
@Entity('conversations')
export class Conversation {
// Identifiant automatique
@PrimaryGeneratedColumn()
id: number;
// Titre de la conversation
@Column({ type: 'text' })
titre: string;
// Date de création
@CreateDateColumn()
createdAt: Date;
// Une conversation possède plusieurs messages
@OneToMany(
() => Message,
(message) => message.conversation,
)
messages: Message[];
}Dans le fichier conversations/entities/message.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Conversation } from './conversation.entity';
@Entity('messages')
export class Message {
@PrimaryGeneratedColumn()
id: number;
// user ou assistant
@Column({ type: 'varchar', length: 20 })
role: string;
// Texte du message
@Column({ type: 'longtext' })
contenu: string;
// Identifiant de la conversation
@Column()
conversationId: number;
// Relation avec la conversation
@ManyToOne(
() => Conversation,
(conversation) => conversation.messages,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'conversationId' })
conversation: Conversation;
@CreateDateColumn()
createdAt: Date;
}Ici, onDelete: 'CASCADE' signifie que lorsqu’on supprime une conversation, ses messages sont également supprimés.
La connection avec NestJS à Hermes avec Ollama
Nous allons envoyer les messages à l’API locale d’Ollama :
POST http://127.0.0.1:11434/api/chat
Dans le fichier ollama/ollama.service.ts
import {
Injectable,
ServiceUnavailableException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
type ChatMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
};
@Injectable()
export class OllamaService {
constructor(private config: ConfigService) {}
async demander(messages: ChatMessage[]): Promise<string> {
// Adresse de notre serveur Ollama
const url = this.config.get(
'OLLAMA_URL',
'http://127.0.0.1:11434',
);
// Nom du modèle à utiliser
const model = this.config.get(
'OLLAMA_MODEL',
'nous-hermes2',
);
try {
// Envoi des messages à Ollama
const response = await fetch(`${url}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model,
messages,
// Retourne une seule réponse JSON
stream: false,
}),
// Évite d'attendre indéfiniment
signal: AbortSignal.timeout(120000),
});
if (!response.ok) {
throw new Error(`Ollama HTTP ${response.status}`);
}
// Conversion de la réponse en objet
const data = await response.json() as {
message?: { content?: string };
};
// Récupération du texte de l'IA
const texte = data.message?.content;
if (!texte) {
throw new Error('Réponse Ollama vide');
}
return texte;
} catch (error) {
console.error('Erreur Ollama :', error);
throw new ServiceUnavailableException(
'Impossible de contacter le modèle IA local.',
);
}
}
}Les explication des instructions importantes :
fetch()effectue une requête HTTP vers Ollama.modelindique quel modèle doit répondre.messagescontient la question et l’historique de discussion.stream: falsedemande une réponse complète, plutôt que plusieurs fragments successifs.await response.json()transforme la réponse JSON en objet JavaScript.return texterenvoie le texte à notre service NestJS.
Dans le fichier ollama/ollama.module.ts :
import { Module } from '@nestjs/common';
import { OllamaService } from './ollama.service';
@Module({
providers: [OllamaService],
// Permet aux autres modules
// d'utiliser OllamaService
exports: [OllamaService],
})
export class OllamaModule {}Le développement de la gestion des conversations
Nous voulons qu’Hermes puisse se souvenir des messages précédents d’une même conversation.
Pour cela, NestJS va lire les messages enregistrés dans MySQL, les transmettre à Ollama et enregistrer la réponse.
Dans le fichier conversations/dto/send-message.dto.ts
import {
IsInt,
IsOptional,
IsString,
MaxLength,
Min,
MinLength,
} from 'class-validator';
export class SendMessageDto {
// Question obligatoire
@IsString()
@MinLength(1)
@MaxLength(10000)
message: string;
// Facultatif : poursuivre une conversation
@IsOptional()
@IsInt()
@Min(1)
conversationId?: number;
}Dans le fichier conversations/conversations.service.ts
import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Conversation } from './entities/conversation.entity';
import { Message } from './entities/message.entity';
import { OllamaService } from '../ollama/ollama.service';
import { SendMessageDto } from './dto/send-message.dto';
@Injectable()
export class ConversationsService {
constructor(
@InjectRepository(Conversation)
private conversations: Repository<Conversation>,
@InjectRepository(Message)
private messages: Repository<Message>,
private ollama: OllamaService,
) {}
// Afficher toutes les conversations
async findAll() {
return this.conversations.find({
order: { createdAt: 'DESC' },
});
}
// Afficher les messages d'une conversation
async findOne(id: number) {
const conversation = await this.conversations.findOne({
where: { id },
relations: { messages: true },
});
if (!conversation) {
throw new NotFoundException('Conversation introuvable');
}
// Trier les messages dans l'ordre de création
conversation.messages.sort((a, b) => a.id - b.id);
return conversation;
}
// Envoyer une question à Hermes
async envoyer(dto: SendMessageDto) {
let conversation: Conversation;
if (dto.conversationId) {
// Continuer une conversation existante
conversation = await this.findOne(dto.conversationId);
} else {
// Créer une nouvelle conversation
conversation = await this.conversations.save({
titre: dto.message.slice(0, 60),
});
}
// Récupérer l'historique déjà enregistré
const historique = await this.messages.find({
where: { conversationId: conversation.id },
order: { id: 'ASC' },
});
// Construire les messages pour Ollama
const messagesIA = [
{
role: 'system' as const,
content: 'Tu es Hermes, un assistant utile. Réponds en français avec des explications simples.',
},
...historique.map((message) => ({
role: message.role as 'user' | 'assistant',
content: message.contenu,
})),
{
role: 'user' as const,
content: dto.message,
},
];
// Interroger le modèle local
const reponse = await this.ollama.demander(messagesIA);
// Enregistrer la question et la réponse
await this.messages.save([
{
conversationId: conversation.id,
role: 'user',
contenu: dto.message,
},
{
conversationId: conversation.id,
role: 'assistant',
contenu: reponse,
},
]);
return {
conversationId: conversation.id,
question: dto.message,
reponse,
};
}
// Supprimer une conversation
async supprimer(id: number) {
await this.findOne(id);
await this.conversations.delete(id);
return { message: 'Conversation supprimée' };
}
}L »historique complet est transmis à Ollama. Pour de longues conversations, il faudra limiter les messages envoyés afin de respecter la fenêtre de contexte du modèle. Il faudra aussi prévoir un mécanisme empêchant deux demandes simultanées de modifier la même conversation.
Dans le fichier conversations/conversations.controller.ts
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
ParseIntPipe,
} from '@nestjs/common';
import { ConversationsService } from './conversations.service';
import { SendMessageDto } from './dto/send-message.dto';
@Controller('conversations')
export class ConversationsController {
constructor(private service: ConversationsService) {}
// GET /conversations
@Get()
findAll() {
return this.service.findAll();
}
// GET /conversations/1
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.service.findOne(id);
}
// POST /conversations/chat
@Post('chat')
envoyer(@Body() dto: SendMessageDto) {
return this.service.envoyer(dto);
}
// DELETE /conversations/1
@Delete(':id')
supprimer(@Param('id', ParseIntPipe) id: number) {
return this.service.supprimer(id);
}
}Dans le fichier conversations/conversations.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Conversation } from './entities/conversation.entity';
import { Message } from './entities/message.entity';
import { ConversationsController } from './conversations.controller';
import { ConversationsService } from './conversations.service';
import { OllamaModule } from '../ollama/ollama.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Conversation,
Message,
]),
OllamaModule,
],
controllers: [ConversationsController],
providers: [ConversationsService],
})
export class ConversationsModule {}Dans le fichier src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Autoriser uniquement notre application Angular
app.enableCors({
origin: 'http://localhost:4200',
});
// Vérifier automatiquement les DTO
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// Écouter uniquement sur l'ordinateur local
await app.listen(
Number(process.env.PORT || 3000),
'127.0.0.1',
);
}
bootstrap();Lancer NestJS
npm run start:devLe serveur doit démarrer sur :
http://127.0.0.1:3000
Voici les routes :
Méthode URL Action
GET /conversations Liste des conversations
GET /conversations/1 Messages de la conversation 1
POST /conversations/chat Poser une question
DELETE /conversations/1 Supprimer la conversation
La création du projet Angular
Maintenant que NestJS fonctionne, nous allons créer l’interface utilisateur avec Angular, en utilisant les Signals pour gérer les données de façon réactive.
Ouvre un deuxième terminal PowerShell :
cd hermes-local
ng new frontend --standalone --routing --style=scss
cd frontendGénère ensuite les composants et services :
ng g c pages/chat
ng g s services/chatVoici à quoi va ressembler notre structure front-end :
frontend/
└── src/
└── app/
├── app.ts
├── app.html
├── app.routes.ts
│
├── models/
│ └── chat.model.ts
│
├── services/
│ └── chat.ts
│
└── pages/
└── chat/
├── chat.ts
├── chat.html
└── chat.scss
La création des interfaces TypeScript
Dans le fichier src/app/models/chat.model.ts :
// Représente un message enregistré
export interface Message {
id: number;
role: 'user' | 'assistant';
contenu: string;
conversationId: number;
createdAt: string;
}
// Représente une conversation
export interface Conversation {
id: number;
titre: string;
createdAt: string;
messages?: Message[];
}
// Réponse renvoyée par NestJS
export interface ChatResponse {
conversationId: number;
question: string;
reponse: string;
}Ces interfaces servent à définir la structure des objets utilisés dans Angular.
La creation du service Angular
Dans le fichier src/app/services/chat.ts :
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
Conversation,
ChatResponse,
} from '../models/chat.model';
@Injectable({
providedIn: 'root',
})
export class ChatService {
private http = inject(HttpClient);
// Adresse de notre API NestJS
private api = 'http://127.0.0.1:3000/conversations';
// Envoyer une question à Hermes
envoyer(message: string, conversationId?: number) {
return this.http.post<ChatResponse>(
`${this.api}/chat`,
{
message,
...(conversationId ? { conversationId } : {}),
},
);
}
// Charger toutes les conversations
liste() {
return this.http.get<Conversation[]>(this.api);
}
// Charger une conversation et ses messages
detail(id: number) {
return this.http.get<Conversation>(
`${this.api}/${id}`,
);
}
// Supprimer une conversation
supprimer(id: number) {
return this.http.delete(`${this.api}/${id}`);
}
}Le développement du composant Chat avec Signals
Dans le fichier src/app/pages/chat/chat.ts :
import {
Component,
inject,
signal,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { firstValueFrom } from 'rxjs';
import { ChatService } from '../../services/chat';
import {
Conversation,
Message,
} from '../../models/chat.model';
@Component({
selector: 'app-chat',
standalone: true,
imports: [FormsModule],
templateUrl: './chat.html',
styleUrl: './chat.scss',
})
export class Chat {
private service = inject(ChatService);
// Liste des conversations
conversations = signal<Conversation[]>([]);
// Messages de la conversation sélectionnée
messages = signal<Message[]>([]);
// Identifiant de la conversation active
conversationId = signal<number | null>(null);
// Question saisie par l'utilisateur
question = signal('');
// Indique si Hermes prépare une réponse
chargement = signal(false);
// Message d'erreur éventuel
erreur = signal('');
constructor() {
this.chargerConversations();
}
// Charger l'historique
async chargerConversations() {
try {
const data = await firstValueFrom(
this.service.liste(),
);
this.conversations.set(data);
} catch {
this.erreur.set(
'Impossible de charger les conversations.',
);
}
}
// Sélectionner une conversation
async ouvrir(id: number) {
if (this.chargement()) return;
this.erreur.set('');
try {
const conversation = await firstValueFrom(
this.service.detail(id),
);
this.conversationId.set(id);
this.messages.set(conversation.messages ?? []);
} catch {
this.erreur.set('Chargement impossible.');
}
}
// Démarrer une nouvelle discussion
nouvelleConversation() {
if (this.chargement()) return;
this.conversationId.set(null);
this.messages.set([]);
this.question.set('');
this.erreur.set('');
}
// Envoyer une question
async envoyer() {
const texte = this.question().trim();
// Ne rien faire si le champ est vide
if (!texte || this.chargement()) return;
this.chargement.set(true);
this.erreur.set('');
try {
// Envoyer la question à NestJS
const resultat = await firstValueFrom(
this.service.envoyer(
texte,
this.conversationId() ?? undefined,
),
);
// Mémoriser l'identifiant de conversation
this.conversationId.set(resultat.conversationId);
// Vider le champ de saisie
this.question.set('');
// Recharger les messages depuis MySQL
await this.ouvrirApresEnvoi(resultat.conversationId);
// Actualiser la liste des conversations
await this.chargerConversations();
} catch {
this.erreur.set(
'Hermes ne répond pas. Vérifie Ollama et NestJS.',
);
} finally {
this.chargement.set(false);
}
}
private async ouvrirApresEnvoi(id: number) {
const conversation = await firstValueFrom(
this.service.detail(id),
);
this.messages.set(conversation.messages ?? []);
}
// Supprimer une conversation
async supprimer(id: number) {
if (this.chargement()) return;
try {
await firstValueFrom(this.service.supprimer(id));
if (this.conversationId() === id) {
this.nouvelleConversation();
}
await this.chargerConversations();
} catch {
this.erreur.set('Suppression impossible.');
}
}
}Explication des Signals
Voici les trois instructions importantes :
// Créer une donnée réactive
question = signal('');
// Lire sa valeur
console.log(this.question());
// Modifier sa valeur
this.question.set('Bonjour Hermes');Quand vous modifiez un Signal avec .set(), Angular actualise automatiquement les parties de l’interface qui utilisent cette donnée.
L’interface HTML
Dans le fichier src/app/pages/chat/chat.html :
<div class="application">
<!-- Menu latéral -->
<aside class="sidebar">
<h2>Hermes Local</h2>
<button
(click)="nouvelleConversation()"
[disabled]="chargement()">
+ Nouvelle conversation
</button>
<h3>Historique</h3>
@for (conversation of conversations(); track conversation.id) {
<div class="conversation">
<span (click)="ouvrir(conversation.id)">
{{ conversation.titre }}
</span>
<button
class="delete"
(click)="supprimer(conversation.id)"
[disabled]="chargement()">
×
</button>
</div>
}
</aside>
<!-- Zone principale -->
<main class="chat">
<header>
<h1>Assistant Hermes</h1>
<p>IA locale avec Ollama</p>
</header>
<section class="messages">
@if (messages().length === 0) {
<div class="welcome">
<h2>Bonjour !</h2>
<p>Comment puis-je t'aider aujourd'hui ?</p>
</div>
}
@for (message of messages(); track message.id) {
<div
class="message"
[class.user]="message.role === 'user'"
[class.assistant]="message.role === 'assistant'">
<strong>
{{ message.role === 'user' ? 'Vous' : 'Hermes' }}
</strong>
<p>{{ message.contenu }}</p>
</div>
}
@if (chargement()) {
<p class="loading">
Hermes réfléchit...
</p>
}
</section>
@if (erreur()) {
<p class="error">{{ erreur() }}</p>
}
<!-- Formulaire d'envoi -->
<form
class="formulaire"
(ngSubmit)="envoyer()">
<textarea
name="question"
[ngModel]="question()"
(ngModelChange)="question.set($event)"
placeholder="Pose ta question à Hermes..."
[disabled]="chargement()">
</textarea>
<button
type="submit"
[disabled]="chargement() || !question().trim()">
Envoyer
</button>
</form>
</main>
</div>On ajouter le style SCSS
Dans le fichier src/app/pages/chat/chat.scss :
:host {
display: block;
height: 100vh;
font-family: Arial, sans-serif;
}
.application {
display: flex;
height: 100%;
background: #f5f6fa;
}
.sidebar {
width: 260px;
background: #172033;
color: white;
padding: 20px;
overflow-y: auto;
button {
padding: 12px;
border: none;
border-radius: 8px;
cursor: pointer;
}
.conversation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #394256;
span {
cursor: pointer;
overflow-wrap: anywhere;
}
.delete {
background: transparent;
color: white;
font-size: 20px;
}
}
}
.chat {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
header {
padding: 20px;
background: white;
border-bottom: 1px solid #ddd;
h1 {
margin: 0;
}
}
.messages {
flex: 1;
overflow-y: auto;
padding: 25px;
}
.message {
max-width: 75%;
margin-bottom: 18px;
padding: 15px;
border-radius: 12px;
overflow-wrap: anywhere;
p {
white-space: pre-wrap;
}
}
.user {
background: #dbeafe;
margin-left: auto;
}
.assistant {
background: white;
margin-right: auto;
}
.welcome {
text-align: center;
margin-top: 60px;
}
.formulaire {
display: flex;
gap: 10px;
padding: 20px;
background: white;
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ccc;
border-radius: 8px;
resize: vertical;
min-height: 50px;
}
button {
padding: 12px 20px;
background: #4f46e5;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
.loading {
color: #666;
}
.error {
color: #b91c1c;
padding: 10px 20px;
}
@media (max-width: 700px) {
.sidebar {
width: 140px;
padding: 10px;
}
.messages {
padding: 12px;
}
.message {
max-width: 95%;
}
}Dans src/styles.scss :
html,
body {
margin: 0;
height: 100%;
}Nous allons configurer la route Angular
Dans src/app/app.routes.ts :
import { Routes } from '@angular/router';
import { Chat } from './pages/chat/chat';
export const routes: Routes = [
{
path: '',
component: Chat,
},
];Dans src/app/app.html, nous allons remplacer le contenu généré par :
<router-outlet />Il fout vérifie que RouterOutlet figure dans les imports du composant racine app.ts.
Démarrer l’application complète
Vous allez avoir trois services locaux en fonctionnement : MySQL, Ollama et NestJS, auxquels nous ajoutons Angular.
Dans un terminal, vérifiez qu’Ollama est actif :
ollama listSi Ollama n’est pas déjà démarré :
ollama serveN.B. : Sur Windows, Ollama peut déjà fonctionner en arrière-plan. Dans ce cas, il ne faut pas lancer un deuxième serveur sur le même port.
Dans un deuxième terminal :
cd hermes-local/backend
npm run start:devDans un troisième terminal :
cd hermes-local/frontend
ng serveN.B. : l’application utilise uniquement des services locaux, mais elle reste pour l’instant un prototype de développement.