File size: 4,770 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | "use client";
import { useTranslations } from "next-intl";
import type { MemoryEngineStatus } from "@/shared/schemas/memory";
type ConfigureTarget = "embedding" | "qdrant" | "rerank";
interface Props {
status: MemoryEngineStatus;
onConfigure?: (target: ConfigureTarget) => void;
}
type ChipColor = "green" | "gray" | "red";
function StatusChip({ color }: { color: ChipColor }) {
const colorMap: Record<ChipColor, string> = {
green: "bg-emerald-500",
gray: "bg-border",
red: "bg-red-500",
};
return (
<span
className={`inline-block w-2.5 h-2.5 rounded-full shrink-0 ${colorMap[color]}`}
aria-hidden="true"
/>
);
}
function ConfigureLink({
onClick,
label,
testId,
}: {
onClick: () => void;
label: string;
testId: string;
}) {
return (
<button
type="button"
data-testid={testId}
onClick={onClick}
className="text-xs text-violet-400 hover:text-violet-300 underline-offset-2 hover:underline mt-1 inline-flex items-center gap-0.5"
>
{label}
<span aria-hidden="true">→</span>
</button>
);
}
export default function MemoryEngineStatus({ status, onConfigure }: Props) {
const t = useTranslations("memory");
const embeddingOff = !status.embedding.available;
const qdrantOff = !status.qdrant.enabled;
const qdrantUnhealthy = status.qdrant.enabled && status.qdrant.healthy === false;
const rerankOff = !status.rerank.enabled || !status.rerank.available;
const rows: Array<{ label: string; chip: ChipColor; reason: string; cta?: React.ReactNode }> = [
{
label: t("engine.keywordLabel"),
chip: "green",
reason: t("engine.keywordReason"),
},
{
label: t("engine.embeddingLabel"),
chip: status.embedding.available ? "green" : "gray",
reason: status.embedding.reason,
cta:
embeddingOff && onConfigure ? (
<ConfigureLink
testId="engine-cta-embedding"
onClick={() => onConfigure("embedding")}
label={t("engine.configureCta")}
/>
) : undefined,
},
{
label: t("engine.vectorStoreLabel"),
chip:
status.vectorStore.available
? "green"
: status.vectorStore.backend === "none"
? "gray"
: "red",
reason: status.vectorStore.reason,
cta:
status.vectorStore.backend === "none" ? (
<span className="text-xs text-amber-400 flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">terminal</span>
{t("engine.vectorStoreInstallHint")}
</span>
) : status.vectorStore.needsReindex > 0 ? (
<span className="text-xs text-amber-400 flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">warning</span>
{t("engine.needsReindex", { count: status.vectorStore.needsReindex })}
</span>
) : undefined,
},
{
label: t("engine.qdrantLabel"),
chip: !status.qdrant.enabled ? "gray" : status.qdrant.healthy ? "green" : "red",
reason: !status.qdrant.enabled
? t("engine.qdrantDisabled")
: status.qdrant.healthy
? t("engine.qdrantOk", { latencyMs: status.qdrant.latencyMs ?? 0 })
: (status.qdrant.error ?? t("engine.qdrantError")),
cta:
(qdrantOff || qdrantUnhealthy) && onConfigure ? (
<ConfigureLink
testId="engine-cta-qdrant"
onClick={() => onConfigure("qdrant")}
label={t("engine.configureCta")}
/>
) : undefined,
},
{
label: t("engine.rerankLabel"),
chip: !status.rerank.enabled ? "gray" : status.rerank.available ? "green" : "red",
reason: status.rerank.reason,
cta:
rerankOff && onConfigure ? (
<ConfigureLink
testId="engine-cta-rerank"
onClick={() => onConfigure("rerank")}
label={t("engine.configureCta")}
/>
) : undefined,
},
];
return (
<div className="space-y-2">
{rows.map((row, i) => (
<div
key={i}
className="flex items-start gap-3 p-3 rounded-lg border border-border/60 bg-surface/30"
>
<StatusChip color={row.chip} />
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-text-main">{row.label}</span>
<p className="text-xs text-text-muted mt-0.5">{row.reason}</p>
{row.cta && <div className="mt-1">{row.cta}</div>}
</div>
</div>
))}
</div>
);
}
|