Mise en ligne de la version 0.2.0
This commit is contained in:
103
web/src/app/settings/settings.component.html
Normal file
103
web/src/app/settings/settings.component.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<div class="settings-page">
|
||||
|
||||
<header class="page-header">
|
||||
<button class="btn-back" (click)="goBack()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="16"></lucide-icon>
|
||||
<span>Retour</span>
|
||||
</button>
|
||||
<h1>Parametres</h1>
|
||||
</header>
|
||||
|
||||
<div *ngIf="errorMessage" class="alert alert-error">
|
||||
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
<div *ngIf="successMessage" class="alert alert-success">
|
||||
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
||||
<span>{{ successMessage }}</span>
|
||||
</div>
|
||||
|
||||
<section class="card" *ngIf="settings">
|
||||
<h2>Moteur IA</h2>
|
||||
<p class="hint">Choix du fournisseur de modele de langage utilise par le chat et la generation de pages.</p>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Fournisseur</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio">
|
||||
<input type="radio" name="provider" value="ollama" [(ngModel)]="settings.llm_provider">
|
||||
<span>Ollama (local)</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="provider" value="onemin" [(ngModel)]="settings.llm_provider">
|
||||
<span>1min.ai (cloud)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bloc Ollama -->
|
||||
<section class="card" *ngIf="settings && settings.llm_provider === 'ollama'">
|
||||
<h2>Configuration Ollama</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ollama-url">URL du serveur Ollama</label>
|
||||
<input id="ollama-url" type="text" [(ngModel)]="settings.ollama_base_url" placeholder="http://localhost:11434">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ollama-model">Modele</label>
|
||||
<div class="inline-select">
|
||||
<select id="ollama-model" [(ngModel)]="settings.llm_model">
|
||||
<option *ngIf="ollamaModels.length === 0" [value]="settings.llm_model">{{ settings.llm_model }}</option>
|
||||
<option *ngFor="let m of ollamaModels" [value]="m">{{ m }}</option>
|
||||
</select>
|
||||
<button type="button" class="btn-secondary" (click)="refreshModels()" [disabled]="loadingModels">
|
||||
<lucide-icon [img]="RefreshCw" [size]="14"></lucide-icon>
|
||||
<span>{{ loadingModels ? 'Chargement...' : 'Actualiser' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint" *ngIf="ollamaModels.length === 0">Aucun modele detecte. Verifie que Ollama tourne et que l'URL est correcte.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bloc 1min.ai -->
|
||||
<section class="card" *ngIf="settings && settings.llm_provider === 'onemin'">
|
||||
<h2>Configuration 1min.ai</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="onemin-key">Cle API</label>
|
||||
<input
|
||||
id="onemin-key"
|
||||
type="password"
|
||||
[(ngModel)]="oneminApiKeyInput"
|
||||
[placeholder]="settings.onemin_api_key_set ? 'Cle configuree (laisser vide pour ne pas changer)' : 'Saisir votre cle API'">
|
||||
<label class="checkbox" *ngIf="settings.onemin_api_key_set">
|
||||
<input type="checkbox" [(ngModel)]="clearApiKey">
|
||||
<span>Effacer la cle enregistree</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="onemin-provider">Fournisseur</label>
|
||||
<select id="onemin-provider" [(ngModel)]="oneminProvider" (ngModelChange)="onProviderChange()">
|
||||
<option *ngFor="let g of oneminGroups" [value]="g.provider">{{ g.provider }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="onemin-model">Modele</label>
|
||||
<select id="onemin-model" [(ngModel)]="settings.onemin_model">
|
||||
<option *ngFor="let m of currentProviderModels" [value]="m">{{ m }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="actions" *ngIf="settings">
|
||||
<button class="btn-primary" (click)="save()" [disabled]="saving">
|
||||
<lucide-icon [img]="Save" [size]="16"></lucide-icon>
|
||||
<span>{{ saving ? 'Sauvegarde...' : 'Sauvegarder' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
138
web/src/app/settings/settings.component.scss
Normal file
138
web/src/app/settings/settings.component.scss
Normal file
@@ -0,0 +1,138 @@
|
||||
.settings-page {
|
||||
padding: 32px 48px;
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
color: var(--color-text, #e8e8e8);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
padding: 24px 28px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
margin: 4px 0 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
label { font-size: 0.9rem; font-weight: 500; }
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select {
|
||||
padding: 9px 12px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: inherit;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent, #7a5cff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group { display: flex; gap: 24px; }
|
||||
.radio, .checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.inline-select {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
select { flex: 1; }
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 9px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-accent, #7a5cff);
|
||||
color: #fff;
|
||||
&:hover:not(:disabled) { filter: brightness(1.1); }
|
||||
&:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
}
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
&:hover:not(:disabled) { background: rgba(255, 255, 255, 0.05); }
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.alert-error { background: rgba(220, 80, 80, 0.15); color: #ff9b9b; }
|
||||
.alert-success { background: rgba(80, 200, 120, 0.15); color: #a2e8b6; }
|
||||
153
web/src/app/settings/settings.component.ts
Normal file
153
web/src/app/settings/settings.component.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, RefreshCw, Save, Check, AlertCircle } from 'lucide-angular';
|
||||
import { SettingsService, AppSettings, AppSettingsUpdate, OneMinModelGroup } from '../services/settings.service';
|
||||
|
||||
/**
|
||||
* Ecran de parametrage du LLM utilise par le Brain.
|
||||
*
|
||||
* Deux providers au choix :
|
||||
* - Ollama (local) : on liste dynamiquement les modeles installes.
|
||||
* - 1min.ai (cloud) : on fournit une cle API + on choisit dans un catalogue fixe.
|
||||
*
|
||||
* Les modifications sont persistees cote Brain dans data/settings.json
|
||||
* (fichier local, usage mono-utilisateur) et appliquees a la prochaine
|
||||
* requete chat / generate — pas besoin de redemarrer.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly RefreshCw = RefreshCw;
|
||||
readonly Save = Save;
|
||||
readonly Check = Check;
|
||||
readonly AlertCircle = AlertCircle;
|
||||
|
||||
settings: AppSettings | null = null;
|
||||
ollamaModels: string[] = [];
|
||||
oneminGroups: OneMinModelGroup[] = [];
|
||||
/** Fournisseur 1min.ai actuellement selectionne (filtre la liste des modeles). */
|
||||
oneminProvider: string = '';
|
||||
|
||||
loadingModels = false;
|
||||
saving = false;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
|
||||
/** Cle 1min.ai saisie — vide = on ne touche pas a la cle persistee. */
|
||||
oneminApiKeyInput = '';
|
||||
/** True si l'utilisateur a coche "effacer la cle". */
|
||||
clearApiKey = false;
|
||||
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
loadSettings(): void {
|
||||
this.settingsService.getSettings().subscribe({
|
||||
next: (s) => {
|
||||
this.settings = { ...s };
|
||||
this.refreshModels();
|
||||
},
|
||||
error: (err) => this.errorMessage = this.extractError(err, 'Impossible de charger les parametres.')
|
||||
});
|
||||
}
|
||||
|
||||
refreshModels(): void {
|
||||
if (!this.settings) return;
|
||||
this.loadingModels = true;
|
||||
|
||||
this.settingsService.listOllamaModels().subscribe({
|
||||
next: (r) => this.ollamaModels = r.models,
|
||||
error: () => this.ollamaModels = [],
|
||||
complete: () => this.loadingModels = false
|
||||
});
|
||||
|
||||
this.settingsService.listOneMinModels().subscribe({
|
||||
next: (r) => {
|
||||
this.oneminGroups = r.groups;
|
||||
this.syncOneminProviderFromModel();
|
||||
},
|
||||
error: () => this.oneminGroups = []
|
||||
});
|
||||
}
|
||||
|
||||
/** Deduit le fournisseur a partir du modele actuellement configure. */
|
||||
private syncOneminProviderFromModel(): void {
|
||||
if (!this.settings) return;
|
||||
const currentModel = this.settings.onemin_model;
|
||||
const found = this.oneminGroups.find(g => g.models.includes(currentModel));
|
||||
this.oneminProvider = found ? found.provider : (this.oneminGroups[0]?.provider ?? '');
|
||||
}
|
||||
|
||||
/** Retourne la liste des modeles du fournisseur selectionne. */
|
||||
get currentProviderModels(): string[] {
|
||||
const group = this.oneminGroups.find(g => g.provider === this.oneminProvider);
|
||||
return group ? group.models : [];
|
||||
}
|
||||
|
||||
/** Quand on change de fournisseur, bascule automatiquement sur son premier modele. */
|
||||
onProviderChange(): void {
|
||||
if (!this.settings) return;
|
||||
const models = this.currentProviderModels;
|
||||
if (models.length > 0 && !models.includes(this.settings.onemin_model)) {
|
||||
this.settings.onemin_model = models[0];
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.settings) return;
|
||||
this.saving = true;
|
||||
this.errorMessage = '';
|
||||
this.successMessage = '';
|
||||
|
||||
const patch: AppSettingsUpdate = {
|
||||
llm_provider: this.settings.llm_provider,
|
||||
ollama_base_url: this.settings.ollama_base_url,
|
||||
llm_model: this.settings.llm_model,
|
||||
onemin_model: this.settings.onemin_model
|
||||
};
|
||||
if (this.clearApiKey) {
|
||||
patch.onemin_api_key = '';
|
||||
} else if (this.oneminApiKeyInput.trim()) {
|
||||
patch.onemin_api_key = this.oneminApiKeyInput.trim();
|
||||
}
|
||||
|
||||
this.settingsService.updateSettings(patch).subscribe({
|
||||
next: (s) => {
|
||||
this.settings = { ...s };
|
||||
this.oneminApiKeyInput = '';
|
||||
this.clearApiKey = false;
|
||||
this.successMessage = 'Parametres sauvegardes.';
|
||||
this.saving = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.errorMessage = this.extractError(err, 'Echec de la sauvegarde.');
|
||||
this.saving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.router.navigate(['/lore']);
|
||||
}
|
||||
|
||||
private extractError(err: any, fallback: string): string {
|
||||
if (err?.error?.detail) return String(err.error.detail);
|
||||
if (err?.message) return err.message;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user