Some checks failed
E2E Tests / e2e (push) Failing after 21s
Build & Push Images / build (brain) (push) Successful in 1m0s
Build & Push Images / build (core) (push) Successful in 1m34s
Build & Push Images / build-switcher (push) Successful in 39s
Build & Push Images / build (web) (push) Successful in 1m40s
469 lines
21 KiB
HTML
469 lines
21 KiB
HTML
<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" (ngModelChange)="fetchOllamaModelInfo()">
|
|
<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>
|
|
<button type="button" class="btn-secondary" (click)="openPullDialog()">
|
|
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
|
<span>Telecharger</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>
|
|
|
|
<!-- Liste des modeles installes avec bouton supprimer -->
|
|
<div class="form-row" *ngIf="ollamaModels.length > 0">
|
|
<label>Modeles installes</label>
|
|
<ul class="installed-models">
|
|
<li *ngFor="let m of ollamaModels">
|
|
<span class="model-name">{{ m }}</span>
|
|
<button type="button" class="btn-icon btn-danger"
|
|
(click)="deleteModel(m)"
|
|
[disabled]="deletingModel === m"
|
|
[title]="'Supprimer ' + m">
|
|
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Dialog de telechargement de modele -->
|
|
<div class="modal-overlay" *ngIf="pullDialogOpen" (click)="closePullDialog()">
|
|
<div class="modal-content" (click)="$event.stopPropagation()">
|
|
<header class="modal-header">
|
|
<h3>Telecharger un modele Ollama</h3>
|
|
<button type="button" class="btn-icon" (click)="closePullDialog()" [disabled]="pullInProgress" title="Fermer">
|
|
<lucide-icon [img]="X" [size]="18"></lucide-icon>
|
|
</button>
|
|
</header>
|
|
|
|
<div class="modal-body">
|
|
<div *ngIf="!pullInProgress">
|
|
<label for="pull-name">Nom du modele</label>
|
|
<input id="pull-name" type="text" [(ngModel)]="pullModelName"
|
|
placeholder="ex: gemma4:e4b" autocomplete="off"
|
|
(keydown.enter)="startPull()">
|
|
|
|
<p class="hint">Suggestions :</p>
|
|
<div class="suggestions">
|
|
<button type="button" *ngFor="let s of pullSuggestions"
|
|
class="suggestion-chip" (click)="selectSuggestion(s)">{{ s }}</button>
|
|
</div>
|
|
<p class="hint" style="margin-top: 0.75rem;">
|
|
La liste complete est sur <a href="https://ollama.com/library" target="_blank" rel="noopener">ollama.com/library</a>.
|
|
</p>
|
|
</div>
|
|
|
|
<div *ngIf="pullInProgress" class="pull-progress">
|
|
<div class="pull-status">{{ pullStatus }}</div>
|
|
<div class="progress-bar" *ngIf="pullTotal > 0">
|
|
<div class="progress-fill" [style.width.%]="pullPercent"></div>
|
|
</div>
|
|
<div class="progress-text" *ngIf="pullTotal > 0">
|
|
{{ formatBytes(pullCompleted) }} / {{ formatBytes(pullTotal) }} ({{ pullPercent }}%)
|
|
</div>
|
|
<div class="progress-text" *ngIf="pullTotal === 0">
|
|
Preparation...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="modal-footer">
|
|
<button type="button" class="btn-secondary"
|
|
(click)="cancelPull()" *ngIf="pullInProgress">
|
|
Annuler
|
|
</button>
|
|
<button type="button" class="btn-secondary"
|
|
(click)="closePullDialog()" *ngIf="!pullInProgress">
|
|
Fermer
|
|
</button>
|
|
<button type="button" class="btn-primary"
|
|
(click)="startPull()"
|
|
[disabled]="pullInProgress || !pullModelName.trim()" *ngIf="!pullInProgress">
|
|
<lucide-icon [img]="Download" [size]="14"></lucide-icon>
|
|
<span>Telecharger</span>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Bloc Fenetre de contexte -->
|
|
<section class="card" *ngIf="settings">
|
|
<h2>Fenetre de contexte</h2>
|
|
|
|
<!-- Ollama : slider borne par le max du modele -->
|
|
<div class="form-row" *ngIf="settings.llm_provider === 'ollama'">
|
|
<label for="llm-num-ctx">
|
|
Tokens alloues au modele
|
|
<span class="ctx-value">{{ settings.llm_num_ctx | number }}</span>
|
|
<span class="ctx-max" *ngIf="ollamaModelMaxContext > 0">
|
|
/ {{ ollamaModelMaxContext | number }} max
|
|
</span>
|
|
</label>
|
|
<input
|
|
id="llm-num-ctx"
|
|
type="range"
|
|
[min]="CTX_MIN"
|
|
[max]="effectiveMaxContext"
|
|
step="1024"
|
|
[(ngModel)]="settings.llm_num_ctx"
|
|
class="ctx-slider">
|
|
<p class="hint" *ngIf="ollamaModelMaxContext > 0">
|
|
Le modele <strong>{{ settings.llm_model }}</strong> accepte jusqu'a
|
|
{{ ollamaModelMaxContext | number }} tokens. Plus la valeur est elevee, plus
|
|
l'IA peut tenir d'historique et de contexte — au prix de VRAM et de latence.
|
|
</p>
|
|
<p class="hint" *ngIf="ollamaModelMaxContext === 0">
|
|
Impossible de determiner la fenetre max du modele (Ollama injoignable ou modele
|
|
inconnu). Slider borne a {{ CTX_FALLBACK_MAX | number }} par securite.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 1min.ai : saisie libre (pas d'introspection possible) -->
|
|
<div class="form-row" *ngIf="settings.llm_provider === 'onemin'">
|
|
<label for="llm-num-ctx-onemin">Fenetre de contexte (tokens)</label>
|
|
<input
|
|
id="llm-num-ctx-onemin"
|
|
type="number"
|
|
min="2048"
|
|
step="1024"
|
|
[(ngModel)]="settings.llm_num_ctx">
|
|
<p class="hint">
|
|
A regler selon la capacite du modele 1min.ai choisi (ex: 128 000 pour gpt-4o,
|
|
200 000 pour claude-sonnet). Sert de plafond a la jauge de contexte du chat.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Bloc Mises a jour (canal stable + canal beta Patreon fusionnes) -->
|
|
<section class="card" *ngIf="config.updateCheckEnabled || licenseStatus?.enabled">
|
|
<h2>Mises a jour</h2>
|
|
<p class="hint">Verifie aupres du registry Docker si une nouvelle version
|
|
des conteneurs (core, brain, web) est disponible. Postgres et MinIO sont
|
|
exclus — ils sont mis a jour manuellement.</p>
|
|
|
|
<!-- ====================================================== -->
|
|
<!-- Sous-section : canal stable -->
|
|
<!-- ====================================================== -->
|
|
<div class="channel-block" *ngIf="config.updateCheckEnabled">
|
|
<h3 class="channel-title">Canal stable</h3>
|
|
|
|
<div class="form-row">
|
|
<button type="button" class="btn-secondary" (click)="checkUpdates()" [disabled]="updateChecking">
|
|
<lucide-icon [img]="RefreshCw" [size]="14"></lucide-icon>
|
|
<span>{{ updateChecking ? 'Verification...' : 'Verifier maintenant' }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div *ngIf="updateStatus && !updateStatus.enabled" class="hint">
|
|
Feature non configuree (WATCHTOWER_TOKEN absent).
|
|
</div>
|
|
|
|
<div *ngIf="updateStatus?.enabled">
|
|
<div *ngIf="updateStatus?.updateAvailable" class="alert alert-success">
|
|
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
|
<span>Une mise a jour est disponible.</span>
|
|
</div>
|
|
<div *ngIf="updateStatus?.anyUnknown && !updateStatus?.updateAvailable" class="alert alert-warn">
|
|
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
|
<span>Verification impossible (baseline absente ou registry injoignable).</span>
|
|
</div>
|
|
<div *ngIf="!updateStatus?.updateAvailable && !updateStatus?.anyUnknown" class="hint">
|
|
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
|
</div>
|
|
|
|
<div class="form-row" *ngIf="updateStatus?.updateAvailable">
|
|
<button type="button" class="btn-primary" (click)="applyUpdate()" [disabled]="updateApplying">
|
|
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
|
<span>{{ updateApplying ? 'Mise a jour en cours...' : 'Mettre a jour maintenant' }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div *ngIf="updateMessage" class="alert alert-success">
|
|
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
|
<span>{{ updateMessage }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ====================================================== -->
|
|
<!-- Sous-section : canal beta (Patreon) -->
|
|
<!-- ====================================================== -->
|
|
<div class="channel-block" *ngIf="licenseStatus?.enabled">
|
|
<h3 class="channel-title">
|
|
<lucide-icon [img]="Heart" [size]="16"></lucide-icon>
|
|
Canal beta — reserve aux patrons
|
|
</h3>
|
|
<p class="hint">
|
|
Soutiens LoreMind sur Patreon pour acceder aux nouvelles features en avant-premiere.
|
|
Le tier <strong>Compagnon</strong> (7€/mois) ou superieur debloque ce canal.
|
|
</p>
|
|
|
|
<!-- Pas de licence installee -->
|
|
<ng-container *ngIf="licenseStatus?.status === 'NONE'">
|
|
<div class="form-row">
|
|
<button type="button" class="btn-primary" (click)="connectPatreon()">
|
|
<lucide-icon [img]="Link2" [size]="16"></lucide-icon>
|
|
<span>Connecter mon compte Patreon</span>
|
|
</button>
|
|
</div>
|
|
<p class="hint">
|
|
Une nouvelle fenetre va s'ouvrir vers Patreon. Apres autorisation, copie le token affiche
|
|
et colle-le ci-dessous.
|
|
</p>
|
|
<div class="form-row">
|
|
<label for="license-jwt">Token Patreon</label>
|
|
<input
|
|
id="license-jwt"
|
|
type="text"
|
|
[(ngModel)]="licenseJwtInput"
|
|
placeholder="eyJhbGciOiJFZERTQS..."
|
|
autocomplete="off"
|
|
>
|
|
</div>
|
|
<div class="form-row">
|
|
<button type="button" class="btn-primary" (click)="installLicense()" [disabled]="!licenseJwtInput.trim()">
|
|
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
|
<span>Activer la licence</span>
|
|
</button>
|
|
</div>
|
|
<div *ngIf="licenseError" class="alert alert-error">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>{{ licenseError }}</span>
|
|
</div>
|
|
</ng-container>
|
|
|
|
<!-- Licence installee (VALID / GRACE / EXPIRED / UNVERIFIABLE) -->
|
|
<ng-container *ngIf="licenseStatus && licenseStatus.status !== 'NONE'">
|
|
<div *ngIf="licenseStatus.status === 'VALID'" class="alert alert-success">
|
|
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
|
<span>Compte Patreon connecte. Tier {{ tierLabel(licenseStatus.tierId) }} actif.</span>
|
|
</div>
|
|
<div *ngIf="licenseStatus.status === 'GRACE'" class="alert alert-warn">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>
|
|
Connexion Patreon expiree, mais acces beta maintenu pendant la periode de tolerance.
|
|
Verifie que ton abonnement Patreon est toujours actif et clique sur "Verifier maintenant".
|
|
</span>
|
|
</div>
|
|
<div *ngIf="licenseStatus.status === 'EXPIRED'" class="alert alert-error">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>
|
|
Connexion Patreon expiree depuis trop longtemps. Reconnecte-toi pour retrouver l'acces beta.
|
|
</span>
|
|
</div>
|
|
<div *ngIf="licenseStatus.status === 'UNVERIFIABLE'" class="alert alert-error">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>Le token installe ne peut plus etre verifie. Reconnecte-toi.</span>
|
|
</div>
|
|
|
|
<ul class="license-info">
|
|
<li *ngIf="licenseStatus.tierId"><strong>Tier :</strong> {{ tierLabel(licenseStatus.tierId) }}</li>
|
|
<li *ngIf="licenseStatus.expiresAt">
|
|
<strong>Validite :</strong>
|
|
jusqu'au {{ formatDate(licenseStatus.expiresAt) }}
|
|
<span *ngIf="daysUntilExpiry !== null && daysUntilExpiry > 0">
|
|
(renouvellement dans {{ daysUntilExpiry }} jour<span *ngIf="daysUntilExpiry > 1">s</span>)
|
|
</span>
|
|
</li>
|
|
<li *ngIf="licenseStatus.lastRefreshAttemptAt">
|
|
<strong>Dernier refresh :</strong>
|
|
{{ formatDate(licenseStatus.lastRefreshAttemptAt) }}
|
|
<span *ngIf="licenseStatus.lastRefreshSucceeded === true" class="badge-ok">OK</span>
|
|
<span *ngIf="licenseStatus.lastRefreshSucceeded === false" class="badge-warn">echec</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="form-row form-row-inline">
|
|
<label class="checkbox">
|
|
<input
|
|
type="checkbox"
|
|
[checked]="licenseStatus.betaChannelEnabled"
|
|
(change)="toggleBetaChannel(!licenseStatus.betaChannelEnabled)"
|
|
[disabled]="licenseStatus.status !== 'VALID' && licenseStatus.status !== 'GRACE'"
|
|
>
|
|
<span>Activer le canal beta</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-row form-row-actions">
|
|
<button type="button" class="btn-secondary" (click)="refreshLicense()" [disabled]="licenseLoading">
|
|
<lucide-icon [img]="RefreshCw" [size]="14"></lucide-icon>
|
|
<span>{{ licenseLoading ? 'Verification...' : 'Verifier maintenant' }}</span>
|
|
</button>
|
|
<button type="button" class="btn-secondary btn-danger" (click)="disconnectPatreon()">
|
|
<lucide-icon [img]="Unlink" [size]="14"></lucide-icon>
|
|
<span>Deconnecter Patreon</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Etat du canal beta -->
|
|
<div *ngIf="licenseStatus.betaChannelEnabled" class="beta-status">
|
|
<div *ngIf="betaChecking" class="hint">Verification des images beta...</div>
|
|
<div *ngIf="!betaChecking && betaStatus && !betaStatus.enabled" class="hint">
|
|
Indisponible : {{ betaStatus.disabledReason }}
|
|
</div>
|
|
<div *ngIf="!betaChecking && betaStatus?.enabled">
|
|
<div *ngIf="betaStatus?.anyUnknown" class="alert alert-warn">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>Verification beta impossible (registry beta injoignable ou baseline absente).</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bascule de canal (stable <-> beta) via sidecar switcher -->
|
|
<div class="channel-switch" *ngIf="channelStatus">
|
|
<div class="channel-current">
|
|
<span class="channel-label">Canal actuel :</span>
|
|
<span class="channel-badge"
|
|
[class.channel-stable]="channelStatus.currentChannel === 'stable'"
|
|
[class.channel-beta]="channelStatus.currentChannel === 'beta'">
|
|
{{ channelStatus.currentChannel === 'beta' ? 'Bêta' : 'Stable' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Sidecar dispo : boutons d'action -->
|
|
<ng-container *ngIf="channelStatus.switcherAvailable">
|
|
<!-- On stable -> proposer passage beta (uniquement si licence active) -->
|
|
<button *ngIf="channelStatus.currentChannel === 'stable'"
|
|
type="button" class="btn-primary"
|
|
[disabled]="switchInFlight"
|
|
(click)="requestChannelSwitch('beta')">
|
|
<lucide-icon [img]="Download" [size]="14"></lucide-icon>
|
|
<span>{{ switchInFlight ? 'Bascule en cours...' : 'Passer sur le canal beta' }}</span>
|
|
</button>
|
|
|
|
<!-- On beta -> proposer retour stable -->
|
|
<button *ngIf="channelStatus.currentChannel === 'beta'"
|
|
type="button" class="btn-secondary"
|
|
[disabled]="switchInFlight"
|
|
(click)="requestChannelSwitch('stable')">
|
|
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
|
|
<span>{{ switchInFlight ? 'Bascule en cours...' : 'Repasser sur le canal stable' }}</span>
|
|
</button>
|
|
|
|
<!-- Switch en cours : on prévient que la page va se rendre injoignable -->
|
|
<div *ngIf="switchInFlight" class="alert alert-warn">
|
|
<lucide-icon [img]="RefreshCw" [size]="16"></lucide-icon>
|
|
<span>Bascule en cours. L'application va etre indisponible 10 a 30 secondes — la page se rechargera automatiquement quand le nouveau Core sera pret.</span>
|
|
</div>
|
|
|
|
<!-- Erreur eventuelle remontee par le sidecar -->
|
|
<div *ngIf="switchError && !switchInFlight" class="alert alert-error">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>{{ switchError }}</span>
|
|
</div>
|
|
</ng-container>
|
|
|
|
<!-- Sidecar PAS dispo : fallback instructions manuelles (vieilles installs) -->
|
|
<div *ngIf="!channelStatus.switcherAvailable" class="alert alert-warn">
|
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
|
<span>
|
|
Le sidecar de bascule n'est pas installe. Pour beneficier du switch
|
|
automatique, recupere le dernier <code>docker-compose.yml</code> du repo
|
|
et fais <code>docker compose pull && docker compose up -d</code> une
|
|
fois. Sinon, bascule manuellement en editant <code>IMAGE_NAMESPACE</code>
|
|
dans ton <code>.env</code> (<code>igmlcreation/loremind-</code> pour stable,
|
|
<code>igmlcreation/loremind-beta-</code> pour beta).
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</ng-container>
|
|
</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>
|