:)
This commit is contained in:
@@ -162,7 +162,7 @@ export function AdminCenter() {
|
||||
<span
|
||||
role="status"
|
||||
aria-label={`${t.label}: ${openAccessCount}`}
|
||||
className="absolute -right-0.5 -top-1.5 grid min-h-5 min-w-5 place-items-center rounded-full bg-red-500 px-1 text-[11px] font-bold leading-5 text-white shadow"
|
||||
className="absolute -right-0.5 -top-0.5 z-10 grid min-h-4 min-w-4 place-items-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-4 text-white shadow"
|
||||
>
|
||||
{openAccessCount > 99 ? "99+" : openAccessCount}
|
||||
</span>
|
||||
@@ -171,7 +171,7 @@ export function AdminCenter() {
|
||||
<span
|
||||
role="status"
|
||||
aria-label={`${t.label}: ${openReportCount}`}
|
||||
className="absolute -right-0.5 -top-1.5 grid min-h-5 min-w-5 place-items-center rounded-full bg-red-500 px-1 text-[11px] font-bold leading-5 text-white shadow"
|
||||
className="absolute -right-0.5 -top-0.5 z-10 grid min-h-4 min-w-4 place-items-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-4 text-white shadow"
|
||||
>
|
||||
{openReportCount > 99 ? "99+" : openReportCount}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Bot, Undo2 } from "lucide-react";
|
||||
import { api, type PolicyModel } from "../lib/api.ts";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Activity, Bot, Undo2 } from "lucide-react";
|
||||
import { api, type OllamaStatus, type PolicyModel } from "../lib/api.ts";
|
||||
import { OllamaModelsModal } from "./AdminPanel.tsx";
|
||||
import { Button, Input, Spinner, Switch } from "./ui.tsx";
|
||||
import { useT } from "../lib/i18n.ts";
|
||||
@@ -48,6 +48,8 @@ export function ModelsPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<OllamaConnectivity />
|
||||
|
||||
<section>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<p className="text-sm font-medium">{t("models.title")}</p>
|
||||
@@ -105,6 +107,140 @@ export function ModelsPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Ollama connectivity probe (admin) ---------- */
|
||||
|
||||
function fmtSize(bytes: number): string {
|
||||
if (!bytes || bytes < 0) return "—";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let v = bytes;
|
||||
let i = 0;
|
||||
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
||||
return `${v >= 100 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
/** Live Ollama host status: reachability, version, latency, on-disk models. */
|
||||
function OllamaConnectivity() {
|
||||
const [status, setStatus] = useState<OllamaStatus | null>(null);
|
||||
const [checking, setChecking] = useState(true);
|
||||
const { t } = useT();
|
||||
|
||||
const check = useCallback(async () => {
|
||||
setChecking(true);
|
||||
try {
|
||||
setStatus(await api.ollamaStatus());
|
||||
} catch {
|
||||
setStatus(null);
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void check();
|
||||
}, [check]);
|
||||
|
||||
const state = !status ? "unknown" : !status.enabled ? "disabled" : status.reachable ? "online" : "offline";
|
||||
const dot =
|
||||
state === "online"
|
||||
? "bg-emerald-500"
|
||||
: state === "offline"
|
||||
? "bg-red-500"
|
||||
: "bg-stone-400 dark:bg-zinc-500";
|
||||
const stateLabel =
|
||||
state === "online"
|
||||
? t("models.connOnline")
|
||||
: state === "offline"
|
||||
? t("models.connOffline")
|
||||
: state === "disabled"
|
||||
? t("models.connDisabledState")
|
||||
: t("common.loading");
|
||||
|
||||
return (
|
||||
<section className="rounded-3xl border border-stone-200/70 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<p className="flex items-center gap-1.5 text-sm font-medium">
|
||||
<Activity size={14} className="opacity-60" /> {t("models.connTitle")}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
|
||||
state === "online" && "bg-emerald-600/10 text-emerald-700 dark:text-emerald-400",
|
||||
state === "offline" && "bg-red-500/10 text-red-600 dark:text-red-400",
|
||||
(state === "disabled" || state === "unknown") && "bg-stone-200/70 dark:bg-zinc-800",
|
||||
)}
|
||||
role="status"
|
||||
>
|
||||
<span className={cn("h-2 w-2 rounded-full", dot, checking && "animate-pulse")} />
|
||||
{checking && !status ? t("common.loading") : stateLabel}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => void check()}
|
||||
disabled={checking}
|
||||
className="ml-auto rounded-full p-2 opacity-60 transition hover:bg-stone-200/60 hover:opacity-100 disabled:opacity-30 dark:hover:bg-zinc-800"
|
||||
title={t("models.connCheck")}
|
||||
aria-label={t("models.connCheckAria")}
|
||||
>
|
||||
<Undo2 size={15} className={checking ? "animate-spin" : undefined} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-3 text-sm opacity-60">{t("models.connDesc")}</p>
|
||||
|
||||
{status && (
|
||||
<dl className="mb-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connHost")}</dt>
|
||||
<dd className="truncate font-mono text-sm" title={status.host}>{status.host}</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connVersion")}</dt>
|
||||
<dd className="truncate font-mono text-sm">{status.version ?? "—"}</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connLatency")}</dt>
|
||||
<dd className="truncate font-mono text-sm">{status.latencyMs === null ? "—" : `${status.latencyMs} ms`}</dd>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connModels")}</dt>
|
||||
<dd className="truncate font-mono text-sm">{status.models.length}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)}
|
||||
|
||||
{!checking && !status && (
|
||||
<p className="mb-3 rounded-2xl bg-red-500/10 px-4 py-2.5 text-sm text-red-600 dark:text-red-400" role="alert">
|
||||
{t("admin.loadFailed")}
|
||||
</p>
|
||||
)}
|
||||
{status && status.error && (
|
||||
<p className="mb-3 rounded-2xl bg-red-500/10 px-4 py-2.5 text-sm text-red-600 dark:text-red-400" role="alert">
|
||||
<span className="block font-mono text-[13px]">{status.error}</span>
|
||||
<span className="mt-0.5 block opacity-80">{t("models.connHint")}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status && status.reachable && (
|
||||
status.models.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-stone-300 px-4 py-4 text-center text-sm opacity-50 dark:border-zinc-700">
|
||||
{t("models.connEmpty")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="max-h-64 space-y-1.5 overflow-y-auto">
|
||||
{status.models.map((m) => (
|
||||
<li
|
||||
key={m.name}
|
||||
className="flex items-center gap-3 rounded-2xl border border-stone-200/70 px-3.5 py-2 dark:border-zinc-800"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-sm">{m.name}</span>
|
||||
<span className="shrink-0 text-xs opacity-60">{fmtSize(m.size)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelRow({ model, onChanged, onError }: {
|
||||
model: PolicyModel;
|
||||
onChanged: (m: PolicyModel) => void;
|
||||
|
||||
@@ -62,6 +62,22 @@ export interface PolicyModel extends DriverModel {
|
||||
daily: number;
|
||||
}
|
||||
|
||||
export interface OllamaHostModel {
|
||||
name: string;
|
||||
size: number;
|
||||
modifiedAt: string | null;
|
||||
}
|
||||
|
||||
export interface OllamaStatus {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
reachable: boolean;
|
||||
version: string | null;
|
||||
latencyMs: number | null;
|
||||
models: OllamaHostModel[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const TOKEN_KEY = "kisassistant_token";
|
||||
|
||||
export function getToken(): string | null {
|
||||
@@ -299,6 +315,9 @@ export const api = {
|
||||
/** Admin: delete a local Ollama model (frees disk on the Ollama host). */
|
||||
ollamaDelete: (name: string) => req<{ ok: boolean }>(`/api/admin/ollama/models/${encodeURIComponent(name)}`, { method: "DELETE" }),
|
||||
|
||||
/** Admin: Ollama connectivity probe (reachability, version, on-disk models). */
|
||||
ollamaStatus: () => req<OllamaStatus>("/api/admin/ollama/status"),
|
||||
|
||||
/** Personal LLM providers (BYOK): presence + last4 only, keys never leave the server. */
|
||||
providers: () => req<{ providers: UserProvider[] }>("/api/me/providers"),
|
||||
setProvider: (provider: string, apiKey: string) =>
|
||||
|
||||
@@ -313,6 +313,19 @@ const en = {
|
||||
"models.ollamaDesc": "Pull or remove local models.",
|
||||
"models.empty": "No models available.",
|
||||
"models.saveFailed": "Could not save",
|
||||
"models.connTitle": "Ollama connectivity",
|
||||
"models.connDesc": "Live status of the Ollama host: reachability, version, latency and installed models.",
|
||||
"models.connOnline": "Online",
|
||||
"models.connOffline": "Offline",
|
||||
"models.connDisabledState": "Disabled",
|
||||
"models.connCheck": "Check now",
|
||||
"models.connCheckAria": "Check Ollama connectivity now",
|
||||
"models.connHost": "Host",
|
||||
"models.connVersion": "Version",
|
||||
"models.connLatency": "Latency",
|
||||
"models.connModels": "Models",
|
||||
"models.connEmpty": "No models on the Ollama host yet — pull one from the library below.",
|
||||
"models.connHint": "Make sure Ollama is running and OLLAMA_HOST points at it.",
|
||||
"center.locked": "Admins only",
|
||||
"center.lockedSub": "Log in with the admin account to open the Admin Center.",
|
||||
"center.regTitle": "Public registration",
|
||||
@@ -862,6 +875,19 @@ const es: Dict = {
|
||||
"models.ollamaDesc": "Descargar o eliminar modelos locales.",
|
||||
"models.empty": "No hay modelos disponibles.",
|
||||
"models.saveFailed": "No se pudo guardar",
|
||||
"models.connTitle": "Conectividad Ollama",
|
||||
"models.connDesc": "Estado en vivo del servidor Ollama: alcance, versión, latencia y modelos instalados.",
|
||||
"models.connOnline": "En línea",
|
||||
"models.connOffline": "Sin conexión",
|
||||
"models.connDisabledState": "Desactivado",
|
||||
"models.connCheck": "Comprobar ahora",
|
||||
"models.connCheckAria": "Comprobar la conectividad Ollama ahora",
|
||||
"models.connHost": "Host",
|
||||
"models.connVersion": "Versión",
|
||||
"models.connLatency": "Latencia",
|
||||
"models.connModels": "Modelos",
|
||||
"models.connEmpty": "Aún no hay modelos en el servidor Ollama — descarga uno desde la biblioteca.",
|
||||
"models.connHint": "Asegúrate de que Ollama esté en marcha y de que OLLAMA_HOST apunte a él.",
|
||||
"center.locked": "Solo admins",
|
||||
"center.lockedSub": "Inicia sesión como admin para abrir el Centro de admin.",
|
||||
"center.regTitle": "Registro público",
|
||||
@@ -1405,6 +1431,19 @@ const fr: Dict = {
|
||||
"models.ollamaDesc": "Télécharger ou supprimer des modèles locaux.",
|
||||
"models.empty": "Aucun modèle disponible.",
|
||||
"models.saveFailed": "Enregistrement impossible",
|
||||
"models.connTitle": "Connectivité Ollama",
|
||||
"models.connDesc": "État en direct de l'hôte Ollama : joignabilité, version, latence et modèles installés.",
|
||||
"models.connOnline": "En ligne",
|
||||
"models.connOffline": "Hors ligne",
|
||||
"models.connDisabledState": "Désactivé",
|
||||
"models.connCheck": "Vérifier",
|
||||
"models.connCheckAria": "Vérifier la connectivité Ollama",
|
||||
"models.connHost": "Hôte",
|
||||
"models.connVersion": "Version",
|
||||
"models.connLatency": "Latence",
|
||||
"models.connModels": "Modèles",
|
||||
"models.connEmpty": "Aucun modèle sur l'hôte Ollama — téléchargez-en un depuis la bibliothèque ci-dessous.",
|
||||
"models.connHint": "Vérifiez qu'Ollama tourne et que OLLAMA_HOST pointe vers lui.",
|
||||
"center.locked": "Admins uniquement",
|
||||
"center.lockedSub": "Connectez-vous en admin pour ouvrir le Centre admin.",
|
||||
"center.regTitle": "Inscription publique",
|
||||
@@ -1939,6 +1978,19 @@ const ru: Dict = {
|
||||
"models.ollamaDesc": "Скачивание и удаление локальных моделей.",
|
||||
"models.empty": "Нет доступных моделей.",
|
||||
"models.saveFailed": "Не удалось сохранить",
|
||||
"models.connTitle": "Подключение Ollama",
|
||||
"models.connDesc": "Состояние хоста Ollama: доступность, версия, задержка и модели.",
|
||||
"models.connOnline": "В сети",
|
||||
"models.connOffline": "Нет связи",
|
||||
"models.connDisabledState": "Отключено",
|
||||
"models.connCheck": "Проверить",
|
||||
"models.connCheckAria": "Проверить подключение Ollama",
|
||||
"models.connHost": "Хост",
|
||||
"models.connVersion": "Версия",
|
||||
"models.connLatency": "Задержка",
|
||||
"models.connModels": "Модели",
|
||||
"models.connEmpty": "На хосте Ollama пока нет моделей — скачайте из библиотеки ниже.",
|
||||
"models.connHint": "Убедитесь, что Ollama запущена и OLLAMA_HOST указывает на неё.",
|
||||
"center.locked": "Только админы",
|
||||
"center.lockedSub": "Войдите как админ, чтобы открыть центр.",
|
||||
"center.regTitle": "Открытая регистрация",
|
||||
@@ -2471,6 +2523,19 @@ const it: Dict = {
|
||||
"models.ollamaDesc": "Scarica o rimuovi modelli locali.",
|
||||
"models.empty": "Nessun modello disponibile.",
|
||||
"models.saveFailed": "Salvataggio fallito",
|
||||
"models.connTitle": "Connettività Ollama",
|
||||
"models.connDesc": "Stato live dell'host Ollama: raggiungibilità, versione, latenza e modelli installati.",
|
||||
"models.connOnline": "Online",
|
||||
"models.connOffline": "Offline",
|
||||
"models.connDisabledState": "Disabilitato",
|
||||
"models.connCheck": "Verifica ora",
|
||||
"models.connCheckAria": "Verifica la connettività Ollama",
|
||||
"models.connHost": "Host",
|
||||
"models.connVersion": "Versione",
|
||||
"models.connLatency": "Latenza",
|
||||
"models.connModels": "Modelli",
|
||||
"models.connEmpty": "Nessun modello sull'host Ollama — scaricane uno dalla libreria qui sotto.",
|
||||
"models.connHint": "Assicurati che Ollama sia in esecuzione e che OLLAMA_HOST punti a esso.",
|
||||
"center.locked": "Solo admin",
|
||||
"center.lockedSub": "Accedi come admin per aprire il Centro.",
|
||||
"center.regTitle": "Registrazione pubblica",
|
||||
@@ -3001,6 +3066,19 @@ const pt: Dict = {
|
||||
"models.ollamaDesc": "Baixar ou remover modelos locais.",
|
||||
"models.empty": "Nenhum modelo disponível.",
|
||||
"models.saveFailed": "Falha ao guardar",
|
||||
"models.connTitle": "Conectividade Ollama",
|
||||
"models.connDesc": "Estado ao vivo do servidor Ollama: alcance, versão, latência e modelos instalados.",
|
||||
"models.connOnline": "Online",
|
||||
"models.connOffline": "Offline",
|
||||
"models.connDisabledState": "Desativado",
|
||||
"models.connCheck": "Verificar agora",
|
||||
"models.connCheckAria": "Verificar a conectividade Ollama agora",
|
||||
"models.connHost": "Servidor",
|
||||
"models.connVersion": "Versão",
|
||||
"models.connLatency": "Latência",
|
||||
"models.connModels": "Modelos",
|
||||
"models.connEmpty": "Nenhum modelo no servidor Ollama — baixe um da biblioteca abaixo.",
|
||||
"models.connHint": "Certifique-se de que o Ollama está a correr e de que OLLAMA_HOST aponta para ele.",
|
||||
"center.locked": "Só admins",
|
||||
"center.lockedSub": "Entra como admin para abrir o Centro.",
|
||||
"center.regTitle": "Registo público",
|
||||
|
||||
Reference in New Issue
Block a user