Archived
CrossRelay stage 2: Codex /v1/responses relay, config.toml manager, Codex page
This commit is contained in:
1 parent
404704171d
commit
5112f7ca06
30 files changed
+3539
-72
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RotateCcw, Zap } from "lucide-react";
|
||||
import type { Provider } from "@/lib/types";
|
||||
import { useProviders, useProviderMutations, useRestore, useStatus } from "@/lib/queries";
|
||||
import { useToast } from "@/components/toast";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function Codex() {
|
||||
const { t } = useTranslation();
|
||||
const status = useStatus();
|
||||
const { data, isLoading, isError } = useProviders();
|
||||
const mutations = useProviderMutations();
|
||||
const restore = useRestore();
|
||||
const { show } = useToast();
|
||||
const [injection, setInjection] = useState<Record<string, string> | null>(null);
|
||||
|
||||
const providers = data?.items ?? [];
|
||||
const active = status.data?.active_codex_provider ?? null;
|
||||
|
||||
const apply = (provider: Provider) => {
|
||||
mutations.apply.mutate(
|
||||
{ id: provider.id, target: "codex" },
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
setInjection(result.injection?.table ?? null);
|
||||
show(`${t("codex.activated")}: ${provider.name}`);
|
||||
},
|
||||
onError: (err: Error) => show(err.message, "error"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const restoreConfig = () => {
|
||||
restore.mutate(
|
||||
{ target: "codex" },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setInjection(null);
|
||||
show(t("codex.restoreDone"));
|
||||
},
|
||||
onError: (err: Error) => show(err.message, "error"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) return <div className="text-sm text-zinc-500">{t("common.loading")}</div>;
|
||||
if (isError) return <div className="text-sm text-red-600">{t("common.error")}</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">{t("codex.title")}</h2>
|
||||
<Button variant="outline" size="sm" disabled={restore.isPending} onClick={restoreConfig}>
|
||||
{restore.isPending ? (
|
||||
<RotateCcw size={14} className="animate-spin" aria-hidden />
|
||||
) : (
|
||||
<Zap size={14} aria-hidden />
|
||||
)}
|
||||
{restore.isPending ? t("common.restoring") : t("codex.restore")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("codex.status")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge tone={active ? "success" : "danger"}>
|
||||
{active ? t("codex.injected") : t("codex.notInjected")}
|
||||
</Badge>
|
||||
{active && <span className="text-sm font-medium">{active.name}</span>}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{t("codex.configPath")}: ~/.codex/config.toml
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">{t("codex.portHint")}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{injection && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("codex.preview")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="grid grid-cols-2 gap-1 font-mono text-xs text-zinc-600">
|
||||
<li>
|
||||
<span className="text-zinc-400">model_provider</span> = crossrelay
|
||||
</li>
|
||||
{Object.entries(injection)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<span className="text-zinc-400">{key}</span> = {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{providers.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-sm text-zinc-500">
|
||||
{t("codex.empty")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{providers.map((provider) => (
|
||||
<Card key={provider.id}>
|
||||
<CardContent className="flex items-center gap-4 py-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{provider.name}</span>
|
||||
{provider.enabled_for_codex && <Badge tone="success">{t("codex.activated")}</Badge>}
|
||||
{provider.preset && <Badge>{provider.preset}</Badge>}
|
||||
</div>
|
||||
<div className="truncate text-xs text-zinc-500">{provider.base_url}</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => apply(provider)}>
|
||||
{t("codex.activate")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user