feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
This commit is contained in:
@ -21,6 +21,7 @@ import type {
|
||||
StopPolicy,
|
||||
} from "@/domain";
|
||||
import { Button, IconButton, Input, Panel, cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import type { ModelServersViewModel } from "./useModelServers";
|
||||
import {
|
||||
deriveServedModelName,
|
||||
@ -42,11 +43,15 @@ function Hint({ children }: { children: React.ReactNode }) {
|
||||
return <small className="text-xs text-muted">{children}</small>;
|
||||
}
|
||||
|
||||
const STOP_POLICIES: { value: StopPolicy; label: string }[] = [
|
||||
{ value: "stopOnAppExit", label: "Stop on app exit" },
|
||||
{ value: "keepAlive", label: "Keep alive" },
|
||||
{ value: "stopWhenUnused", label: "Stop when unused" },
|
||||
];
|
||||
type Tr = (french: string, english: string) => string;
|
||||
|
||||
function stopPolicies(tr: Tr): { value: StopPolicy; label: string }[] {
|
||||
return [
|
||||
{ value: "stopOnAppExit", label: tr("Arrêter à la fermeture de l’application", "Stop on app exit") },
|
||||
{ value: "keepAlive", label: tr("Garder actif", "Keep alive") },
|
||||
{ value: "stopWhenUnused", label: tr("Arrêter si inutilisé", "Stop when unused") },
|
||||
];
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
||||
@ -64,13 +69,14 @@ function formatBytes(bytes: number): string {
|
||||
function deleteArtifactConfirmation(
|
||||
server: LocalModelServerConfig,
|
||||
artifact: Extract<ModelArtifact, { state: "downloaded" }>,
|
||||
tr: Tr,
|
||||
): string {
|
||||
const size =
|
||||
artifact.sizeBytes == null ? "" : `Espace libéré : ${formatBytes(artifact.sizeBytes)}.`;
|
||||
artifact.sizeBytes == null ? "" : `${tr("Espace libéré", "Space freed")}: ${formatBytes(artifact.sizeBytes)}.`;
|
||||
return [
|
||||
"Supprimer le modèle téléchargé ?",
|
||||
`Serveur : ${server.name}`,
|
||||
"Le serveur local restera configuré, mais IdeA devra retélécharger ce modèle au prochain lancement.",
|
||||
tr("Supprimer le modèle téléchargé ?", "Delete downloaded model?"),
|
||||
`${tr("Serveur", "Server")}: ${server.name}`,
|
||||
tr("Le serveur local restera configuré, mais IdeA devra retélécharger ce modèle au prochain lancement.", "The local server will remain configured, but IdeA will have to download this model again on the next launch."),
|
||||
size,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@ -83,6 +89,7 @@ export interface ModelServersPanelProps {
|
||||
}
|
||||
|
||||
export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
const { tr } = useI18n();
|
||||
// The server being edited, or a fresh draft when "Add" is clicked. `null` ⇒
|
||||
// no editor open (list-only).
|
||||
const [draft, setDraft] = useState<LocalModelServerConfig | null>(null);
|
||||
@ -108,25 +115,25 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
server: LocalModelServerConfig,
|
||||
artifact: Extract<ModelArtifact, { state: "downloaded" }>,
|
||||
) {
|
||||
if (!window.confirm(deleteArtifactConfirmation(server, artifact))) return;
|
||||
if (!window.confirm(deleteArtifactConfirmation(server, artifact, tr))) return;
|
||||
await vm.deleteArtifact(server.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
aria-label="local model servers"
|
||||
aria-label={tr("Serveurs de modèles locaux", "Local model servers")}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-content">
|
||||
Local model servers
|
||||
{tr("Serveurs de modèles locaux", "Local model servers")}
|
||||
</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
aria-label="add model server"
|
||||
aria-label={tr("Ajouter un serveur de modèles", "Add model server")}
|
||||
onClick={startAdd}
|
||||
className="ml-auto"
|
||||
>
|
||||
Add server
|
||||
{tr("Ajouter un serveur", "Add server")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@ -145,8 +152,7 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
|
||||
{vm.servers.length === 0 && !draft && (
|
||||
<p className="text-xs text-muted">
|
||||
No local server declared yet. Add one to auto-manage a `llama-server`,
|
||||
or keep using an external endpoint.
|
||||
{tr("Aucun serveur local déclaré. Ajoutez-en un pour gérer automatiquement un `llama-server`, ou continuez à utiliser un point d’accès externe.", "No local server declared yet. Add one to auto-manage a `llama-server`, or keep using an external endpoint.")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -168,7 +174,7 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
</strong>
|
||||
<span className="truncate text-xs text-muted">
|
||||
{server.baseURL} · {server.servedModelName}
|
||||
{server.autoStart ? " · auto-start" : ""}
|
||||
{server.autoStart ? ` · ${tr("démarrage automatique", "auto-start")}` : ""}
|
||||
</span>
|
||||
</span>
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
@ -176,35 +182,35 @@ export function ModelServersPanel({ vm }: ModelServersPanelProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
aria-label={`download in progress ${server.name}`}
|
||||
aria-label={tr(`Téléchargement en cours pour ${server.name}`, `Download in progress for ${server.name}`)}
|
||||
disabled
|
||||
>
|
||||
Téléchargement en cours
|
||||
{tr("Téléchargement en cours", "Downloading")}
|
||||
</Button>
|
||||
)}
|
||||
{downloaded && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
aria-label={`delete downloaded model ${server.name}`}
|
||||
aria-label={tr(`Supprimer le modèle téléchargé de ${server.name}`, `Delete downloaded model for ${server.name}`)}
|
||||
onClick={() => void confirmDeleteArtifact(server, downloaded)}
|
||||
loading={deletingArtifact}
|
||||
disabled={vm.busy && !deletingArtifact}
|
||||
>
|
||||
{deletingArtifact ? "Suppression..." : "Supprimer le modèle téléchargé"}
|
||||
{deletingArtifact ? tr("Suppression…", "Deleting…") : tr("Supprimer le modèle téléchargé", "Delete downloaded model")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
aria-label={`edit ${server.name}`}
|
||||
aria-label={tr(`Modifier ${server.name}`, `Edit ${server.name}`)}
|
||||
onClick={() => startEdit(server)}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
Edit
|
||||
{tr("Modifier", "Edit")}
|
||||
</Button>
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={`delete ${server.name}`}
|
||||
aria-label={tr(`Supprimer ${server.name}`, `Delete ${server.name}`)}
|
||||
onClick={() => void vm.remove(server.id)}
|
||||
disabled={vm.busy}
|
||||
>
|
||||
@ -242,6 +248,7 @@ function SourceTabs({
|
||||
kind: ModelSourceKind;
|
||||
onSelect: (kind: ModelSourceKind) => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const tab = (value: ModelSourceKind, label: string) => {
|
||||
const active = kind === value;
|
||||
return (
|
||||
@ -263,11 +270,11 @@ function SourceTabs({
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="source du modèle"
|
||||
aria-label={tr("Source du modèle", "Model source")}
|
||||
className="flex gap-1 rounded-md border border-border p-1"
|
||||
>
|
||||
{tab("huggingFace", "Télécharger depuis Hugging Face")}
|
||||
{tab("localPath", "Fichier .gguf local")}
|
||||
{tab("huggingFace", tr("Télécharger depuis Hugging Face", "Download from Hugging Face"))}
|
||||
{tab("localPath", tr("Fichier .gguf local", "Local .gguf file"))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -280,8 +287,9 @@ function CommandPreview({
|
||||
preview: ModelServerCommandPreview | null;
|
||||
autoStart: boolean;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const title = autoStart ? "Commande générée" : "Commande à lancer manuellement";
|
||||
const title = autoStart ? tr("Commande générée", "Generated command") : tr("Commande à lancer manuellement", "Command to run manually");
|
||||
|
||||
async function copy() {
|
||||
if (!preview) return;
|
||||
@ -296,7 +304,7 @@ function CommandPreview({
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="command preview"
|
||||
aria-label={tr("Aperçu de la commande", "Command preview")}
|
||||
className="flex flex-col gap-1 rounded-md border border-border bg-raised p-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -304,24 +312,24 @@ function CommandPreview({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="copier la commande"
|
||||
aria-label={tr("Copier la commande", "Copy command")}
|
||||
onClick={() => void copy()}
|
||||
disabled={!preview}
|
||||
className="ml-auto"
|
||||
>
|
||||
{copied ? "Copié" : "Copier"}
|
||||
{copied ? tr("Copié", "Copied") : tr("Copier", "Copy")}
|
||||
</Button>
|
||||
</div>
|
||||
{preview ? (
|
||||
<code
|
||||
aria-label="preview command line"
|
||||
aria-label={tr("Ligne de commande prévisualisée", "Command line preview")}
|
||||
className="whitespace-pre-wrap break-all font-mono text-xs text-content"
|
||||
>
|
||||
{preview.display}
|
||||
</code>
|
||||
) : (
|
||||
<span className="text-xs text-muted">
|
||||
Renseigne une source de modèle pour voir la commande.
|
||||
{tr("Renseignez une source de modèle pour voir la commande.", "Enter a model source to see the command.")}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
@ -346,6 +354,7 @@ function ServerEditor({
|
||||
onCancel: () => void;
|
||||
onSubmit: (nextDraft?: LocalModelServerConfig) => void;
|
||||
}) {
|
||||
const { tr } = useI18n();
|
||||
const errors: ModelServerErrors = validateModelServer(draft);
|
||||
const valid = Object.keys(errors).length === 0;
|
||||
const patch = (next: Partial<LocalModelServerConfig>) =>
|
||||
@ -431,17 +440,17 @@ function ServerEditor({
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
aria-label="model server editor"
|
||||
aria-label={tr("Éditeur de serveur de modèles", "Model server editor")}
|
||||
className="flex flex-col gap-3 rounded-md border border-primary/40 p-3"
|
||||
>
|
||||
<legend className="px-1 text-xs font-medium text-muted">
|
||||
{draft.name.trim().length > 0 ? draft.name : "New server"}
|
||||
{draft.name.trim().length > 0 ? draft.name : tr("Nouveau serveur", "New server")}
|
||||
</legend>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>Nom</Caption>
|
||||
<Caption>{tr("Nom", "Name")}</Caption>
|
||||
<Input
|
||||
aria-label="server name"
|
||||
aria-label={tr("Nom du serveur", "Server name")}
|
||||
value={draft.name}
|
||||
invalid={Boolean(errors.name)}
|
||||
onChange={(e) => patch({ name: e.target.value })}
|
||||
@ -451,15 +460,15 @@ function ServerEditor({
|
||||
|
||||
{/* --- Source du modèle --------------------------------------------- */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<Caption>Source du modèle</Caption>
|
||||
<Caption>{tr("Source du modèle", "Model source")}</Caption>
|
||||
<SourceTabs kind={sourceKind} onSelect={selectKind} />
|
||||
</div>
|
||||
|
||||
{sourceKind === "huggingFace" ? (
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>Modèle Hugging Face</Caption>
|
||||
<Caption>{tr("Modèle Hugging Face", "Hugging Face model")}</Caption>
|
||||
<Input
|
||||
aria-label="hugging face model"
|
||||
aria-label={tr("Modèle Hugging Face", "Hugging Face model")}
|
||||
value={hfRepo}
|
||||
placeholder="unsloth/Qwen3.5-9B-GGUF:Q4_K_M"
|
||||
invalid={Boolean(errors.modelSource)}
|
||||
@ -472,16 +481,16 @@ function ServerEditor({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Hint>llama.cpp téléchargera/cache le fichier automatiquement.</Hint>
|
||||
<Hint>{tr("llama.cpp téléchargera et mettra le fichier en cache automatiquement.", "llama.cpp will download and cache the file automatically.")}</Hint>
|
||||
{errors.modelSource && (
|
||||
<small className="text-xs text-danger">{errors.modelSource}</small>
|
||||
)}
|
||||
</label>
|
||||
) : (
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>Fichier .gguf</Caption>
|
||||
<Caption>{tr("Fichier .gguf", ".gguf file")}</Caption>
|
||||
<Input
|
||||
aria-label="local gguf file"
|
||||
aria-label={tr("Fichier .gguf local", "Local .gguf file")}
|
||||
value={localPath}
|
||||
placeholder="/home/me/models/qwen.gguf"
|
||||
invalid={Boolean(errors.modelSource)}
|
||||
@ -494,7 +503,7 @@ function ServerEditor({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Hint>Chemin absolu vers un .gguf déjà présent.</Hint>
|
||||
<Hint>{tr("Chemin absolu vers un fichier .gguf déjà présent.", "Absolute path to an existing .gguf file.")}</Hint>
|
||||
{errors.modelSource && (
|
||||
<small className="text-xs text-danger">{errors.modelSource}</small>
|
||||
)}
|
||||
@ -503,15 +512,15 @@ function ServerEditor({
|
||||
|
||||
{/* --- Nom du modèle dans IdeA -------------------------------------- */}
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>Nom du modèle dans IdeA</Caption>
|
||||
<Caption>{tr("Nom du modèle dans IdeA", "Model name in IdeA")}</Caption>
|
||||
<Input
|
||||
aria-label="server served model name"
|
||||
aria-label={tr("Nom du modèle servi", "Served model name")}
|
||||
value={draft.servedModelName}
|
||||
placeholder="qwen3-coder-30b"
|
||||
invalid={Boolean(errors.servedModelName)}
|
||||
onChange={(e) => patch({ servedModelName: e.target.value })}
|
||||
/>
|
||||
<Hint>Nom envoyé aux clients OpenAI-compatible.</Hint>
|
||||
<Hint>{tr("Nom envoyé aux clients compatibles OpenAI.", "Name sent to OpenAI-compatible clients.")}</Hint>
|
||||
{errors.servedModelName && (
|
||||
<small className="text-xs text-danger">{errors.servedModelName}</small>
|
||||
)}
|
||||
@ -520,9 +529,9 @@ function ServerEditor({
|
||||
{/* --- Serveur ------------------------------------------------------ */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="flex min-w-[12rem] flex-1 flex-col gap-1">
|
||||
<Caption>Commande serveur</Caption>
|
||||
<Caption>{tr("Commande du serveur", "Server command")}</Caption>
|
||||
<Input
|
||||
aria-label="server binary path"
|
||||
aria-label={tr("Chemin de l’exécutable du serveur", "Server binary path")}
|
||||
value={draft.binaryPath ?? ""}
|
||||
placeholder="llama-server"
|
||||
onChange={(e) => {
|
||||
@ -530,13 +539,13 @@ function ServerEditor({
|
||||
patch({ binaryPath: v.length === 0 ? undefined : v });
|
||||
}}
|
||||
/>
|
||||
<Hint>Laisse vide pour résoudre `llama-server` via le PATH.</Hint>
|
||||
<Hint>{tr("Laissez vide pour trouver `llama-server` via le PATH.", "Leave empty to resolve `llama-server` through PATH.")}</Hint>
|
||||
</label>
|
||||
|
||||
<label className="flex min-w-[6rem] flex-col gap-1">
|
||||
<Caption>Port</Caption>
|
||||
<Caption>{tr("Port", "Port")}</Caption>
|
||||
<Input
|
||||
aria-label="server port"
|
||||
aria-label={tr("Port du serveur", "Server port")}
|
||||
value={Number.isNaN(draft.port) ? "" : draft.port.toString()}
|
||||
inputMode="numeric"
|
||||
invalid={Boolean(errors.port)}
|
||||
@ -547,9 +556,9 @@ function ServerEditor({
|
||||
</div>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>Base URL (OpenAI-compatible)</Caption>
|
||||
<Caption>{tr("URL de base (compatible OpenAI)", "Base URL (OpenAI-compatible)")}</Caption>
|
||||
<Input
|
||||
aria-label="server base url"
|
||||
aria-label={tr("URL de base du serveur", "Server base URL")}
|
||||
value={draft.baseURL}
|
||||
placeholder="http://localhost:8080/v1"
|
||||
invalid={Boolean(errors.baseURL)}
|
||||
@ -563,16 +572,16 @@ function ServerEditor({
|
||||
<label className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="server auto start"
|
||||
aria-label={tr("Démarrage automatique du serveur", "Server auto start")}
|
||||
checked={draft.autoStart}
|
||||
disabled={!draft.modelSource}
|
||||
onChange={(e) => patch({ autoStart: e.target.checked })}
|
||||
className="mt-1 accent-primary disabled:opacity-50"
|
||||
/>
|
||||
<span className="flex flex-col">
|
||||
<Caption>Démarrer automatiquement</Caption>
|
||||
<Caption>{tr("Démarrer automatiquement", "Start automatically")}</Caption>
|
||||
{!draft.modelSource && (
|
||||
<Hint>Renseigne d'abord une source de modèle pour l'activer.</Hint>
|
||||
<Hint>{tr("Renseignez d’abord une source de modèle pour l’activer.", "Enter a model source first to enable it.")}</Hint>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
@ -580,16 +589,16 @@ function ServerEditor({
|
||||
{/* --- Réglages llama.cpp (avancé) --------------------------------- */}
|
||||
<details className="rounded-md border border-border p-2">
|
||||
<summary className="cursor-pointer text-xs font-medium text-muted">
|
||||
Réglages llama.cpp
|
||||
{tr("Réglages llama.cpp", "llama.cpp settings")}
|
||||
</summary>
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="flex min-w-[8rem] flex-1 flex-col gap-1">
|
||||
<Caption>
|
||||
Couches GPU <span className="text-muted">(-ngl)</span>
|
||||
{tr("Couches GPU", "GPU layers")} <span className="text-muted">(-ngl)</span>
|
||||
</Caption>
|
||||
<Input
|
||||
aria-label="gpu layers"
|
||||
aria-label={tr("Couches GPU", "GPU layers")}
|
||||
value={draft.gpuLayers === undefined ? "" : String(draft.gpuLayers)}
|
||||
inputMode="numeric"
|
||||
placeholder="99"
|
||||
@ -600,15 +609,15 @@ function ServerEditor({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Hint>0 = CPU seul.</Hint>
|
||||
<Hint>{tr("0 = processeur uniquement.", "0 = CPU only.")}</Hint>
|
||||
</label>
|
||||
|
||||
<label className="flex min-w-[8rem] flex-1 flex-col gap-1">
|
||||
<Caption>
|
||||
Taille de contexte <span className="text-muted">(-c)</span>
|
||||
{tr("Taille du contexte", "Context size")} <span className="text-muted">(-c)</span>
|
||||
</Caption>
|
||||
<Input
|
||||
aria-label="context size"
|
||||
aria-label={tr("Taille du contexte", "Context size")}
|
||||
value={
|
||||
draft.contextSize === undefined ? "" : String(draft.contextSize)
|
||||
}
|
||||
@ -627,10 +636,10 @@ function ServerEditor({
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>
|
||||
Adresse d'écoute <span className="text-muted">(--host)</span>
|
||||
{tr("Adresse d’écoute", "Listen address")} <span className="text-muted">(--host)</span>
|
||||
</Caption>
|
||||
<Input
|
||||
aria-label="listen host"
|
||||
aria-label={tr("Adresse d’écoute", "Listen address")}
|
||||
value={draft.host}
|
||||
placeholder="127.0.0.1"
|
||||
invalid={Boolean(errors.host)}
|
||||
@ -644,20 +653,20 @@ function ServerEditor({
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="jinja template"
|
||||
aria-label={tr("Modèle de discussion Jinja", "Jinja chat template")}
|
||||
checked={draft.jinja}
|
||||
onChange={(e) => patch({ jinja: e.target.checked })}
|
||||
className="accent-primary"
|
||||
/>
|
||||
<Caption>
|
||||
Template chat Jinja <span className="text-muted">(--jinja)</span>
|
||||
{tr("Modèle de discussion Jinja", "Jinja chat template")} <span className="text-muted">(--jinja)</span>
|
||||
</Caption>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<Caption>Arguments supplémentaires</Caption>
|
||||
<Caption>{tr("Arguments supplémentaires", "Extra arguments")}</Caption>
|
||||
<Input
|
||||
aria-label="extra arguments"
|
||||
aria-label={tr("Arguments supplémentaires", "Extra arguments")}
|
||||
value={argsText}
|
||||
placeholder="--flash-attn --parallel 2"
|
||||
onChange={(e) => setArgsText(e.target.value)}
|
||||
@ -666,16 +675,15 @@ function ServerEditor({
|
||||
/>
|
||||
{reservedHits.length > 0 && (
|
||||
<small className="text-xs text-warning" role="status">
|
||||
{reservedHits.join(", ")} : cette option est déjà pilotée par un
|
||||
champ ci-dessus.
|
||||
{reservedHits.join(", ")} : {tr("cette option est déjà pilotée par un champ ci-dessus.", "this option is already controlled by a field above.")}
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2">
|
||||
<Caption>Stop policy</Caption>
|
||||
<Caption>{tr("Politique d’arrêt", "Stop policy")}</Caption>
|
||||
<select
|
||||
aria-label="server stop policy"
|
||||
aria-label={tr("Politique d’arrêt du serveur", "Server stop policy")}
|
||||
value={draft.stopPolicy}
|
||||
onChange={(e) => patch({ stopPolicy: e.target.value as StopPolicy })}
|
||||
className={cn(
|
||||
@ -683,7 +691,7 @@ function ServerEditor({
|
||||
"border border-border outline-none focus:border-primary",
|
||||
)}
|
||||
>
|
||||
{STOP_POLICIES.map((p) => (
|
||||
{stopPolicies(tr).map((p) => (
|
||||
<option key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</option>
|
||||
@ -699,20 +707,20 @@ function ServerEditor({
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
aria-label="save model server"
|
||||
aria-label={tr("Enregistrer le serveur de modèles", "Save model server")}
|
||||
onClick={() => onSubmit(draftWithCommittedArgs())}
|
||||
disabled={busy || !valid}
|
||||
>
|
||||
Save server
|
||||
{tr("Enregistrer le serveur", "Save server")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="cancel model server"
|
||||
aria-label={tr("Annuler la modification du serveur", "Cancel model server")}
|
||||
onClick={onCancel}
|
||||
disabled={busy}
|
||||
>
|
||||
Cancel
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
Reference in New Issue
Block a user