mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat: add token-saver dashboard page
- extract token saver into its own route /dashboard/token-saver - slim down EndpointPageClient - add token-saver nav to Header and Sidebar Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,17 +4,13 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components";
|
import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components";
|
||||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
|
|
||||||
import {
|
import {
|
||||||
WENYAN_LOCALES,
|
|
||||||
TUNNEL_BENEFITS,
|
TUNNEL_BENEFITS,
|
||||||
TUNNEL_PING_INTERVAL_MS,
|
TUNNEL_PING_INTERVAL_MS,
|
||||||
TUNNEL_PING_MAX_MS,
|
TUNNEL_PING_MAX_MS,
|
||||||
STATUS_POLL_FAST_MS,
|
STATUS_POLL_FAST_MS,
|
||||||
REACHABLE_MISS_THRESHOLD,
|
REACHABLE_MISS_THRESHOLD,
|
||||||
CLIENT_PING_FAST_MS,
|
CLIENT_PING_FAST_MS,
|
||||||
CAVEMAN_LEVELS,
|
|
||||||
PONYTAIL_LEVELS,
|
|
||||||
} from "./endpointConstants";
|
} from "./endpointConstants";
|
||||||
import { clientPingUrl, clientPingAny } from "./endpointPing";
|
import { clientPingUrl, clientPingAny } from "./endpointPing";
|
||||||
import EndpointRow from "./components/EndpointRow";
|
import EndpointRow from "./components/EndpointRow";
|
||||||
@@ -32,22 +28,9 @@ export default function APIPageClient({ machineId }) {
|
|||||||
const [requireApiKey, setRequireApiKey] = useState(false);
|
const [requireApiKey, setRequireApiKey] = useState(false);
|
||||||
const [requireLogin, setRequireLogin] = useState(true);
|
const [requireLogin, setRequireLogin] = useState(true);
|
||||||
const [hasPassword, setHasPassword] = useState(true);
|
const [hasPassword, setHasPassword] = useState(true);
|
||||||
const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false);
|
const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false);
|
||||||
const [rtkEnabled, setRtkEnabledState] = useState(true);
|
|
||||||
const [headroomEnabled, setHeadroomEnabled] = useState(false);
|
|
||||||
const [headroomUrl, setHeadroomUrl] = useState("http://localhost:8787");
|
|
||||||
const [headroomCompressUserMessages, setHeadroomCompressUserMessages] = useState(false);
|
|
||||||
const [headroomStatus, setHeadroomStatus] = useState({ installed: false, running: false, python: null, loading: true });
|
|
||||||
const [showHeadroomInstallModal, setShowHeadroomInstallModal] = useState(false);
|
|
||||||
const [headroomActionLoading, setHeadroomActionLoading] = useState(false);
|
|
||||||
const [headroomActionError, setHeadroomActionError] = useState("");
|
|
||||||
const [cavemanEnabled, setCavemanEnabled] = useState(false);
|
|
||||||
const [cavemanLevel, setCavemanLevel] = useState("full");
|
|
||||||
const [ponytailEnabled, setPonytailEnabled] = useState(false);
|
|
||||||
const [ponytailLevel, setPonytailLevel] = useState("full");
|
|
||||||
const [locale, setLocale] = useState("en");
|
|
||||||
|
|
||||||
// Cloudflare Tunnel state
|
// Cloudflare Tunnel state
|
||||||
const [tunnelChecking, setTunnelChecking] = useState(true);
|
const [tunnelChecking, setTunnelChecking] = useState(true);
|
||||||
const [tunnelEnabled, setTunnelEnabled] = useState(false);
|
const [tunnelEnabled, setTunnelEnabled] = useState(false);
|
||||||
const [tunnelReachable, setTunnelReachable] = useState(false);
|
const [tunnelReachable, setTunnelReachable] = useState(false);
|
||||||
@@ -101,26 +84,6 @@ export default function APIPageClient({ machineId }) {
|
|||||||
setIsRemoteHost(!["localhost", "127.0.0.1", "::1"].includes(window.location.hostname));
|
setIsRemoteHost(!["localhost", "127.0.0.1", "::1"].includes(window.location.hostname));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Track app UI locale to gate wenyan caveman levels
|
|
||||||
useEffect(() => {
|
|
||||||
setLocale(getCurrentLocale());
|
|
||||||
return onLocaleChange(() => setLocale(getCurrentLocale()));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isWenyanLocale = WENYAN_LOCALES.includes(locale);
|
|
||||||
const visibleCavemanLevels = isWenyanLocale
|
|
||||||
? CAVEMAN_LEVELS
|
|
||||||
: CAVEMAN_LEVELS.filter((lvl) => !lvl.wenyan);
|
|
||||||
|
|
||||||
// Reset wenyan level to "ultra" when leaving a Chinese locale
|
|
||||||
useEffect(() => {
|
|
||||||
const current = CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel);
|
|
||||||
if (current?.wenyan && !isWenyanLocale) {
|
|
||||||
setCavemanLevel("ultra");
|
|
||||||
patchSetting({ cavemanLevel: "ultra" });
|
|
||||||
}
|
|
||||||
}, [isWenyanLocale, cavemanLevel]);
|
|
||||||
|
|
||||||
const { copied, copy } = useCopyToClipboard();
|
const { copied, copy } = useCopyToClipboard();
|
||||||
|
|
||||||
// Security gate: block remote exposure while dashboard uses default password or login is off.
|
// Security gate: block remote exposure while dashboard uses default password or login is off.
|
||||||
@@ -241,15 +204,6 @@ export default function APIPageClient({ machineId }) {
|
|||||||
setRequireLogin(data.requireLogin !== false);
|
setRequireLogin(data.requireLogin !== false);
|
||||||
setHasPassword(data.hasPassword || false);
|
setHasPassword(data.hasPassword || false);
|
||||||
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
|
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
|
||||||
setRtkEnabledState(data.rtkEnabled !== false);
|
|
||||||
setHeadroomEnabled(!!data.headroomEnabled);
|
|
||||||
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
|
|
||||||
setHeadroomCompressUserMessages(!!data.headroomCompressUserMessages);
|
|
||||||
refreshHeadroomStatus();
|
|
||||||
setCavemanEnabled(!!data.cavemanEnabled);
|
|
||||||
setCavemanLevel(data.cavemanLevel || "full");
|
|
||||||
setPonytailEnabled(!!data.ponytailEnabled);
|
|
||||||
setPonytailLevel(data.ponytailLevel || "full");
|
|
||||||
}
|
}
|
||||||
if (statusRes.ok) {
|
if (statusRes.ok) {
|
||||||
const data = await statusRes.json();
|
const data = await statusRes.json();
|
||||||
@@ -299,106 +253,6 @@ export default function APIPageClient({ machineId }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRtkEnabled = async (value) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/settings", {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ rtkEnabled: value }),
|
|
||||||
});
|
|
||||||
if (res.ok) setRtkEnabledState(value);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error updating rtkEnabled:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const patchSetting = async (patch) => {
|
|
||||||
try {
|
|
||||||
await fetch("/api/settings", {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(patch),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error updating setting:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCavemanEnabled = (value) => {
|
|
||||||
setCavemanEnabled(value);
|
|
||||||
patchSetting({ cavemanEnabled: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeadroomEnabled = (value) => {
|
|
||||||
const nextUrl = headroomUrl.trim() || "http://localhost:8787";
|
|
||||||
setHeadroomUrl(nextUrl);
|
|
||||||
setHeadroomEnabled(value);
|
|
||||||
patchSetting({ headroomEnabled: value, headroomUrl: nextUrl });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeadroomUrlBlur = async () => {
|
|
||||||
const next = headroomUrl.trim() || "http://localhost:8787";
|
|
||||||
setHeadroomUrl(next);
|
|
||||||
await patchSetting({ headroomUrl: next });
|
|
||||||
refreshHeadroomStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeadroomCompressUserMessages = (value) => {
|
|
||||||
setHeadroomCompressUserMessages(value);
|
|
||||||
patchSetting({ headroomCompressUserMessages: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshHeadroomStatus = useCallback(async () => {
|
|
||||||
setHeadroomStatus((s) => ({ ...s, loading: true }));
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/headroom/status", { headers: { "Cache-Control": "no-store" } });
|
|
||||||
const data = await res.json();
|
|
||||||
setHeadroomStatus({ ...data, loading: false });
|
|
||||||
} catch {
|
|
||||||
setHeadroomStatus({ installed: false, running: false, python: null, loading: false });
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleHeadroomStart = useCallback(async () => {
|
|
||||||
setHeadroomActionError("");
|
|
||||||
setHeadroomActionLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/headroom/start", { method: "POST" });
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
if (!res.ok) throw new Error(data.error || "Failed to start proxy");
|
|
||||||
await refreshHeadroomStatus();
|
|
||||||
} catch (e) {
|
|
||||||
setHeadroomActionError(e.message);
|
|
||||||
} finally {
|
|
||||||
setHeadroomActionLoading(false);
|
|
||||||
}
|
|
||||||
}, [refreshHeadroomStatus]);
|
|
||||||
|
|
||||||
const handleHeadroomStop = useCallback(async () => {
|
|
||||||
setHeadroomActionLoading(true);
|
|
||||||
try {
|
|
||||||
await fetch("/api/headroom/stop", { method: "POST" });
|
|
||||||
await refreshHeadroomStatus();
|
|
||||||
} finally {
|
|
||||||
setHeadroomActionLoading(false);
|
|
||||||
}
|
|
||||||
}, [refreshHeadroomStatus]);
|
|
||||||
|
|
||||||
const handleCavemanLevel = (level) => {
|
|
||||||
setCavemanLevel(level);
|
|
||||||
patchSetting({ cavemanLevel: level });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePonytailEnabled = (value) => {
|
|
||||||
setPonytailEnabled(value);
|
|
||||||
patchSetting({ ponytailEnabled: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePonytailLevel = (level) => {
|
|
||||||
setPonytailLevel(level);
|
|
||||||
patchSetting({ ponytailLevel: level });
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const keysRes = await fetch("/api/keys");
|
const keysRes = await fetch("/api/keys");
|
||||||
@@ -846,19 +700,6 @@ export default function APIPageClient({ machineId }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentEndpoint = baseUrl;
|
const currentEndpoint = baseUrl;
|
||||||
const headroomRunning = !!headroomStatus.running;
|
|
||||||
const headroomLocalUrl = headroomStatus.localUrl !== false;
|
|
||||||
const headroomCanStart = !!headroomStatus.canStart;
|
|
||||||
const headroomManaged = headroomLocalUrl && !!headroomStatus.managedPid;
|
|
||||||
const headroomStatusLabel = headroomStatus.loading
|
|
||||||
? "Checking…"
|
|
||||||
: headroomRunning
|
|
||||||
? "Running"
|
|
||||||
: headroomLocalUrl && !headroomStatus.installed
|
|
||||||
? "Not installed"
|
|
||||||
: headroomLocalUrl
|
|
||||||
? "Proxy off"
|
|
||||||
: "Unreachable";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
@@ -1220,167 +1061,6 @@ export default function APIPageClient({ machineId }) {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Token Saver (RTK + Caveman) */}
|
|
||||||
<Card id="rtk">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
||||||
<span className="material-symbols-outlined text-primary">bolt</span>
|
|
||||||
Token Saver
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between pt-2 pb-4 border-b border-border gap-4">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="font-medium">
|
|
||||||
Compress tool output{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/rtk-ai/rtk"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
|
||||||
>
|
|
||||||
(RTK)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-text-muted">
|
|
||||||
git/grep/ls/tree/logs → 60-90% fewer input tokens
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
checked={rtkEnabled}
|
|
||||||
onChange={() => handleRtkEnabled(!rtkEnabled)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between py-4 border-b border-border gap-4 flex-wrap">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
|
||||||
<p className="font-medium">
|
|
||||||
Compress context{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/chopratejas/headroom"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
|
||||||
>
|
|
||||||
(Headroom)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<span className={`text-xs px-2 py-0.5 rounded ${headroomRunning ? "bg-success/15 text-success" : "bg-warning/15 text-warning"}`}>
|
|
||||||
{headroomStatusLabel}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowHeadroomInstallModal(true)}
|
|
||||||
className="text-xs text-primary underline hover:opacity-80"
|
|
||||||
>
|
|
||||||
{headroomRunning ? "Manage" : "Setup"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-text-muted mt-1">
|
|
||||||
Compress prompts via /v1/compress before routing to the model
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Toggle
|
|
||||||
checked={headroomEnabled && headroomRunning}
|
|
||||||
disabled={!headroomRunning}
|
|
||||||
onChange={() => handleHeadroomEnabled(!headroomEnabled)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between pt-4 gap-4 flex-wrap">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="font-medium">
|
|
||||||
Compress LLM output{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/JuliusBrussee/caveman"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
|
||||||
>
|
|
||||||
(Caveman)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-text-muted">
|
|
||||||
Terse-style system prompt → ~65% fewer output tokens (up to 87%)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 shrink-0">
|
|
||||||
{cavemanEnabled && (
|
|
||||||
<div className="flex flex-col items-end gap-1">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{visibleCavemanLevels.map((lvl) => (
|
|
||||||
<button
|
|
||||||
key={lvl.id}
|
|
||||||
onClick={() => handleCavemanLevel(lvl.id)}
|
|
||||||
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
|
|
||||||
cavemanLevel === lvl.id
|
|
||||||
? "bg-primary text-white border-primary"
|
|
||||||
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
|
|
||||||
}`}
|
|
||||||
title={lvl.desc}
|
|
||||||
>
|
|
||||||
{lvl.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-primary">
|
|
||||||
{CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel)?.desc}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Toggle
|
|
||||||
checked={cavemanEnabled}
|
|
||||||
onChange={() => handleCavemanEnabled(!cavemanEnabled)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-border gap-4 flex-wrap">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="font-medium">
|
|
||||||
Lazy senior dev{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/DietrichGebert/ponytail"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
|
||||||
>
|
|
||||||
(Ponytail)
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-text-muted">
|
|
||||||
Bias the model toward minimal code: YAGNI, reuse stdlib, deletion over addition
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 shrink-0">
|
|
||||||
{ponytailEnabled && (
|
|
||||||
<div className="flex flex-col items-end gap-1">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{PONYTAIL_LEVELS.map((lvl) => (
|
|
||||||
<button
|
|
||||||
key={lvl.id}
|
|
||||||
onClick={() => handlePonytailLevel(lvl.id)}
|
|
||||||
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
|
|
||||||
ponytailLevel === lvl.id
|
|
||||||
? "bg-primary text-white border-primary"
|
|
||||||
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
|
|
||||||
}`}
|
|
||||||
title={lvl.desc}
|
|
||||||
>
|
|
||||||
{lvl.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-primary">
|
|
||||||
{PONYTAIL_LEVELS.find((lvl) => lvl.id === ponytailLevel)?.desc}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Toggle
|
|
||||||
checked={ponytailEnabled}
|
|
||||||
onChange={() => handlePonytailEnabled(!ponytailEnabled)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Add Key Modal */}
|
{/* Add Key Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showAddModal}
|
isOpen={showAddModal}
|
||||||
@@ -1596,67 +1276,6 @@ export default function APIPageClient({ machineId }) {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Headroom Install Guide Modal */}
|
|
||||||
<Modal
|
|
||||||
isOpen={showHeadroomInstallModal}
|
|
||||||
title={headroomRunning ? "Headroom" : "Setup Headroom"}
|
|
||||||
onClose={() => setShowHeadroomInstallModal(false)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<span>Status</span>
|
|
||||||
<span className={headroomRunning ? "text-success" : "text-warning"}>
|
|
||||||
{headroomStatusLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="text-sm font-medium">Proxy URL</p>
|
|
||||||
<Input
|
|
||||||
value={headroomUrl}
|
|
||||||
onChange={(e) => setHeadroomUrl(e.target.value)}
|
|
||||||
onBlur={handleHeadroomUrlBlur}
|
|
||||||
placeholder="http://localhost:8787"
|
|
||||||
className="font-mono text-sm"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-text-muted">
|
|
||||||
Use a local proxy for Start/Stop, or an external Docker sidecar like http://headroom:8787.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{headroomManaged ? (
|
|
||||||
<Button onClick={handleHeadroomStop} variant="ghost" fullWidth disabled={headroomActionLoading}>
|
|
||||||
{headroomActionLoading ? "Stopping…" : "Stop Headroom"}
|
|
||||||
</Button>
|
|
||||||
) : headroomRunning ? (
|
|
||||||
<p className="text-sm text-success">Headroom proxy is reachable. You can enable the token saver.</p>
|
|
||||||
) : headroomCanStart ? (
|
|
||||||
<Button onClick={handleHeadroomStart} fullWidth disabled={headroomActionLoading}>
|
|
||||||
{headroomActionLoading ? "Starting…" : "Start Headroom"}
|
|
||||||
</Button>
|
|
||||||
) : !headroomLocalUrl ? (
|
|
||||||
<p className="text-sm text-warning">Start Headroom separately at the configured URL, then recheck.</p>
|
|
||||||
) : !headroomStatus.python ? (
|
|
||||||
<p className="text-sm text-warning">Python ≥ 3.10 required for local managed mode. Install Python first, or use an external proxy URL.</p>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="text-sm font-medium">Install then click Start:</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<pre className="flex-1 rounded bg-black/5 dark:bg-white/5 p-2 text-xs font-mono overflow-x-auto">{`pip install "headroom-ai[proxy]"`}</pre>
|
|
||||||
<Button size="sm" variant="ghost" onClick={() => copy(`pip install "headroom-ai[proxy]"`)}>
|
|
||||||
{copied ? "Copied" : "Copy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{headroomActionError && (
|
|
||||||
<p className="text-sm text-warning">{headroomActionError}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button onClick={() => refreshHeadroomStatus()} variant="ghost" fullWidth>Recheck</Button>
|
|
||||||
<Button onClick={() => setShowHeadroomInstallModal(false)} fullWidth>Done</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Confirm Modal */}
|
{/* Confirm Modal */}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!confirmState}
|
isOpen={!!confirmState}
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Card, Button, Input, Modal, Toggle } from "@/shared/components";
|
||||||
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
|
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
|
||||||
|
import {
|
||||||
|
WENYAN_LOCALES,
|
||||||
|
CAVEMAN_LEVELS,
|
||||||
|
PONYTAIL_LEVELS,
|
||||||
|
} from "../endpoint/endpointConstants";
|
||||||
|
|
||||||
|
export default function TokenSaverClient() {
|
||||||
|
const [rtkEnabled, setRtkEnabledState] = useState(true);
|
||||||
|
const [headroomEnabled, setHeadroomEnabled] = useState(false);
|
||||||
|
const [headroomUrl, setHeadroomUrl] = useState("http://localhost:8787");
|
||||||
|
const [headroomStatus, setHeadroomStatus] = useState({
|
||||||
|
installed: false,
|
||||||
|
running: false,
|
||||||
|
python: null,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
const [showHeadroomInstallModal, setShowHeadroomInstallModal] =
|
||||||
|
useState(false);
|
||||||
|
const [headroomActionLoading, setHeadroomActionLoading] = useState(false);
|
||||||
|
const [headroomActionError, setHeadroomActionError] = useState("");
|
||||||
|
const [cavemanEnabled, setCavemanEnabled] = useState(false);
|
||||||
|
const [cavemanLevel, setCavemanLevel] = useState("full");
|
||||||
|
const [ponytailEnabled, setPonytailEnabled] = useState(false);
|
||||||
|
const [ponytailLevel, setPonytailLevel] = useState("full");
|
||||||
|
const [locale, setLocale] = useState("en");
|
||||||
|
|
||||||
|
const { copied, copy } = useCopyToClipboard();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocale(getCurrentLocale());
|
||||||
|
return onLocaleChange(() => setLocale(getCurrentLocale()));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isWenyanLocale = WENYAN_LOCALES.includes(locale);
|
||||||
|
const visibleCavemanLevels = isWenyanLocale
|
||||||
|
? CAVEMAN_LEVELS
|
||||||
|
: CAVEMAN_LEVELS.filter((lvl) => !lvl.wenyan);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel);
|
||||||
|
if (current?.wenyan && !isWenyanLocale) {
|
||||||
|
setCavemanLevel("ultra");
|
||||||
|
patchSetting({ cavemanLevel: "ultra" });
|
||||||
|
}
|
||||||
|
}, [isWenyanLocale, cavemanLevel]);
|
||||||
|
|
||||||
|
const patchSetting = async (patch) => {
|
||||||
|
try {
|
||||||
|
await fetch("/api/settings", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error updating setting:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRtkEnabled = async (value) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ rtkEnabled: value }),
|
||||||
|
});
|
||||||
|
if (res.ok) setRtkEnabledState(value);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error updating rtkEnabled:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCavemanEnabled = (value) => {
|
||||||
|
setCavemanEnabled(value);
|
||||||
|
patchSetting({ cavemanEnabled: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeadroomEnabled = (value) => {
|
||||||
|
const nextUrl = headroomUrl.trim() || "http://localhost:8787";
|
||||||
|
setHeadroomUrl(nextUrl);
|
||||||
|
setHeadroomEnabled(value);
|
||||||
|
patchSetting({ headroomEnabled: value, headroomUrl: nextUrl });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeadroomUrlBlur = async () => {
|
||||||
|
const next = headroomUrl.trim() || "http://localhost:8787";
|
||||||
|
setHeadroomUrl(next);
|
||||||
|
await patchSetting({ headroomUrl: next });
|
||||||
|
refreshHeadroomStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshHeadroomStatus = useCallback(async () => {
|
||||||
|
setHeadroomStatus((s) => ({ ...s, loading: true }));
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/headroom/status", {
|
||||||
|
headers: { "Cache-Control": "no-store" },
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
setHeadroomStatus({ ...data, loading: false });
|
||||||
|
} catch {
|
||||||
|
setHeadroomStatus({
|
||||||
|
installed: false,
|
||||||
|
running: false,
|
||||||
|
python: null,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleHeadroomStart = useCallback(async () => {
|
||||||
|
setHeadroomActionError("");
|
||||||
|
setHeadroomActionLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/headroom/start", { method: "POST" });
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) throw new Error(data.error || "Failed to start proxy");
|
||||||
|
await refreshHeadroomStatus();
|
||||||
|
} catch (e) {
|
||||||
|
setHeadroomActionError(e.message);
|
||||||
|
} finally {
|
||||||
|
setHeadroomActionLoading(false);
|
||||||
|
}
|
||||||
|
}, [refreshHeadroomStatus]);
|
||||||
|
|
||||||
|
const handleHeadroomStop = useCallback(async () => {
|
||||||
|
setHeadroomActionLoading(true);
|
||||||
|
try {
|
||||||
|
await fetch("/api/headroom/stop", { method: "POST" });
|
||||||
|
await refreshHeadroomStatus();
|
||||||
|
} finally {
|
||||||
|
setHeadroomActionLoading(false);
|
||||||
|
}
|
||||||
|
}, [refreshHeadroomStatus]);
|
||||||
|
|
||||||
|
const handleCavemanLevel = (level) => {
|
||||||
|
setCavemanLevel(level);
|
||||||
|
patchSetting({ cavemanLevel: level });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePonytailEnabled = (value) => {
|
||||||
|
setPonytailEnabled(value);
|
||||||
|
patchSetting({ ponytailEnabled: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePonytailLevel = (level) => {
|
||||||
|
setPonytailLevel(level);
|
||||||
|
patchSetting({ ponytailLevel: level });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings");
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setRtkEnabledState(data.rtkEnabled !== false);
|
||||||
|
setHeadroomEnabled(!!data.headroomEnabled);
|
||||||
|
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
|
||||||
|
setCavemanEnabled(!!data.cavemanEnabled);
|
||||||
|
setCavemanLevel(data.cavemanLevel || "full");
|
||||||
|
setPonytailEnabled(!!data.ponytailEnabled);
|
||||||
|
setPonytailLevel(data.ponytailLevel || "full");
|
||||||
|
refreshHeadroomStatus();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
loadSettings();
|
||||||
|
}, [refreshHeadroomStatus]);
|
||||||
|
|
||||||
|
const headroomRunning = !!headroomStatus.running;
|
||||||
|
const headroomStatusLabel = headroomStatus.loading
|
||||||
|
? "Checking…"
|
||||||
|
: headroomRunning
|
||||||
|
? "Running"
|
||||||
|
: headroomStatus.localUrl !== false && !headroomStatus.installed
|
||||||
|
? "Not installed"
|
||||||
|
: headroomStatus.localUrl !== false
|
||||||
|
? "Stopped"
|
||||||
|
: "External";
|
||||||
|
const headroomLocalUrl = headroomStatus.localUrl !== false;
|
||||||
|
const headroomCanStart = !!headroomStatus.canStart;
|
||||||
|
const headroomManaged =
|
||||||
|
headroomLocalUrl && !!headroomStatus.managedPid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto space-y-6 p-6">
|
||||||
|
<Card id="rtk">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<span className="material-symbols-outlined text-primary">
|
||||||
|
bolt
|
||||||
|
</span>
|
||||||
|
Token Saver
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between pt-2 pb-4 border-b border-border gap-4">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium">
|
||||||
|
Compress tool output{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/rtk-ai/rtk"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||||
|
>
|
||||||
|
(RTK)
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-text-muted">
|
||||||
|
git/grep/ls/tree/logs → 60-90% fewer input tokens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={rtkEnabled}
|
||||||
|
onChange={() => handleRtkEnabled(!rtkEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-4 border-b border-border gap-4 flex-wrap">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<p className="font-medium">
|
||||||
|
Compress context{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/chopratejas/headroom"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||||
|
>
|
||||||
|
(Headroom)
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-0.5 rounded ${headroomRunning ? "bg-success/15 text-success" : "bg-warning/15 text-warning"}`}
|
||||||
|
>
|
||||||
|
{headroomStatusLabel}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowHeadroomInstallModal(true)}
|
||||||
|
className="text-xs text-primary underline hover:opacity-80"
|
||||||
|
>
|
||||||
|
{headroomRunning ? "Manage" : "Setup"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-text-muted mt-1">
|
||||||
|
Compress prompts via /v1/compress before routing to the model
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={headroomEnabled && headroomRunning}
|
||||||
|
disabled={!headroomRunning}
|
||||||
|
onChange={() => handleHeadroomEnabled(!headroomEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between pt-4 gap-4 flex-wrap">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium">
|
||||||
|
Compress LLM output{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/JuliusBrussee/caveman"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||||
|
>
|
||||||
|
(Caveman)
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-text-muted">
|
||||||
|
Terse-style system prompt → ~65% fewer output tokens (up to 87%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
{cavemanEnabled && (
|
||||||
|
<div className="flex flex-col items-end gap-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{visibleCavemanLevels.map((lvl) => (
|
||||||
|
<button
|
||||||
|
key={lvl.id}
|
||||||
|
onClick={() => handleCavemanLevel(lvl.id)}
|
||||||
|
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
|
||||||
|
cavemanLevel === lvl.id
|
||||||
|
? "bg-primary text-white border-primary"
|
||||||
|
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
|
||||||
|
}`}
|
||||||
|
title={lvl.desc}
|
||||||
|
>
|
||||||
|
{lvl.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-primary">
|
||||||
|
{
|
||||||
|
CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel)
|
||||||
|
?.desc
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Toggle
|
||||||
|
checked={cavemanEnabled}
|
||||||
|
onChange={() => handleCavemanEnabled(!cavemanEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between pt-4 mt-4 border-t border-border gap-4 flex-wrap">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium">
|
||||||
|
Lazy senior dev{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/DietrichGebert/ponytail"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||||
|
>
|
||||||
|
(Ponytail)
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-text-muted">
|
||||||
|
Bias the model toward minimal code: YAGNI, reuse stdlib,
|
||||||
|
deletion over addition
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
{ponytailEnabled && (
|
||||||
|
<div className="flex flex-col items-end gap-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{PONYTAIL_LEVELS.map((lvl) => (
|
||||||
|
<button
|
||||||
|
key={lvl.id}
|
||||||
|
onClick={() => handlePonytailLevel(lvl.id)}
|
||||||
|
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
|
||||||
|
ponytailLevel === lvl.id
|
||||||
|
? "bg-primary text-white border-primary"
|
||||||
|
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
|
||||||
|
}`}
|
||||||
|
title={lvl.desc}
|
||||||
|
>
|
||||||
|
{lvl.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-primary">
|
||||||
|
{
|
||||||
|
PONYTAIL_LEVELS.find((lvl) => lvl.id === ponytailLevel)
|
||||||
|
?.desc
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Toggle
|
||||||
|
checked={ponytailEnabled}
|
||||||
|
onChange={() => handlePonytailEnabled(!ponytailEnabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={showHeadroomInstallModal}
|
||||||
|
title={headroomRunning ? "Headroom" : "Setup Headroom"}
|
||||||
|
onClose={() => setShowHeadroomInstallModal(false)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Status</span>
|
||||||
|
<span
|
||||||
|
className={headroomRunning ? "text-success" : "text-warning"}
|
||||||
|
>
|
||||||
|
{headroomStatusLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="text-sm font-medium">Proxy URL</p>
|
||||||
|
<Input
|
||||||
|
value={headroomUrl}
|
||||||
|
onChange={(e) => setHeadroomUrl(e.target.value)}
|
||||||
|
onBlur={handleHeadroomUrlBlur}
|
||||||
|
placeholder="http://localhost:8787"
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-text-muted">
|
||||||
|
Use a local proxy for Start/Stop, or an external Docker sidecar
|
||||||
|
like http://headroom:8787.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{headroomManaged ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleHeadroomStop}
|
||||||
|
variant="ghost"
|
||||||
|
fullWidth
|
||||||
|
disabled={headroomActionLoading}
|
||||||
|
>
|
||||||
|
{headroomActionLoading ? "Stopping…" : "Stop Headroom"}
|
||||||
|
</Button>
|
||||||
|
) : headroomRunning ? (
|
||||||
|
<p className="text-sm text-success">
|
||||||
|
Headroom proxy is reachable. You can enable the token saver.
|
||||||
|
</p>
|
||||||
|
) : headroomCanStart ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleHeadroomStart}
|
||||||
|
fullWidth
|
||||||
|
disabled={headroomActionLoading}
|
||||||
|
>
|
||||||
|
{headroomActionLoading ? "Starting…" : "Start Headroom"}
|
||||||
|
</Button>
|
||||||
|
) : !headroomLocalUrl ? (
|
||||||
|
<p className="text-sm text-warning">
|
||||||
|
Start Headroom separately at the configured URL, then recheck.
|
||||||
|
</p>
|
||||||
|
) : !headroomStatus.python ? (
|
||||||
|
<p className="text-sm text-warning">
|
||||||
|
Python ≥ 3.10 required for local managed mode. Install Python
|
||||||
|
first, or use an external proxy URL.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="text-sm font-medium">Install then click Start:</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<pre className="flex-1 rounded bg-black/5 dark:bg-white/5 p-2 text-xs font-mono overflow-x-auto">
|
||||||
|
{`pip install "headroom-ai[proxy]"`}
|
||||||
|
</pre>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
copy(`pip install "headroom-ai[proxy]"`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copied ? "Copied" : "Copy"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{headroomActionError && (
|
||||||
|
<p className="text-sm text-warning">{headroomActionError}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => refreshHeadroomStatus()}
|
||||||
|
variant="ghost"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Recheck
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowHeadroomInstallModal(false)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import TokenSaverClient from "./TokenSaverClient";
|
||||||
|
|
||||||
|
export default function TokenSaverPage() {
|
||||||
|
return <TokenSaverClient />;
|
||||||
|
}
|
||||||
@@ -112,6 +112,13 @@ const getPageInfo = (pathname) => {
|
|||||||
icon: "security",
|
icon: "security",
|
||||||
breadcrumbs: [],
|
breadcrumbs: [],
|
||||||
};
|
};
|
||||||
|
if (pathname.includes("/token-saver"))
|
||||||
|
return {
|
||||||
|
title: "Token Saver",
|
||||||
|
description: "Compress prompts and outputs to save tokens",
|
||||||
|
icon: "savings",
|
||||||
|
breadcrumbs: [],
|
||||||
|
};
|
||||||
if (pathname.includes("/cli-tools"))
|
if (pathname.includes("/cli-tools"))
|
||||||
return {
|
return {
|
||||||
title: "CLI Tools",
|
title: "CLI Tools",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const navItems = [
|
|||||||
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
||||||
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
|
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
|
||||||
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
|
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
|
||||||
{ href: "/dashboard/mitm", label: "MITM", icon: "security" },
|
{ href: "/dashboard/token-saver", label: "Token Saver", icon: "savings" },
|
||||||
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user