Refactor global styles and enhance MITM functionality
- Updated global CSS to implement a new brand color palette and improve light/dark theme consistency. - Enhanced the MitmServerCard component to provide clearer user feedback regarding admin privileges. - Filtered LLM combos in the CombosPage to ensure only relevant data is displayed. - Improved APIPageClient layout for better usability and visual consistency. - Added functionality to save and load DNS tool states in the MITM manager. - Updated OAuth configuration URLs for Qwen to reflect the new endpoint structure. - Refined tunnel management logic to improve reliability and user experience.
This commit is contained in:
1 parent
1686adc704
commit
6cdf40b44e
40 files changed
+1029
-762
No files matched your search
@@ -23,6 +23,8 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
const serverIsWindows = status?.isWin === true;
|
||||
const canRunWithoutPassword = serverIsWindows || status?.hasCachedPassword || status?.needsSudoPassword === false;
|
||||
const isAdmin = status?.isAdmin !== false;
|
||||
// No privilege: not admin/root AND (Win OR no cached sudo password)
|
||||
const noPrivilege = !isAdmin && (serverIsWindows || (!status?.hasCachedPassword && status?.needsSudoPassword !== false));
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -217,6 +219,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
<button
|
||||
onClick={() => handleAction("start")}
|
||||
disabled={loading || (serverIsWindows && !isAdmin)}
|
||||
title={serverIsWindows && !isAdmin ? "Administrator required" : undefined}
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-primary/30 bg-primary/10 px-4 py-2 text-xs font-medium text-primary transition-colors hover:bg-primary/20 disabled:opacity-50 sm:w-auto sm:py-1.5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">play_circle</span>
|
||||
|
||||
@@ -32,7 +32,8 @@ export default function CombosPage() {
|
||||
const providersData = await providersRes.json();
|
||||
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
||||
|
||||
if (combosRes.ok) setCombos(combosData.combos || []);
|
||||
// Only LLM combos here — webSearch/webFetch combos belong to media-providers/web
|
||||
if (combosRes.ok) setCombos((combosData.combos || []).filter(c => !c.kind));
|
||||
if (providersRes.ok) {
|
||||
setActiveProviders(providersData.connections || []);
|
||||
}
|
||||
|
||||
@@ -675,8 +675,8 @@ export default function APIPageClient({ machineId }) {
|
||||
/>
|
||||
{/* Cloudflare Tunnel */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[68px] text-center ${
|
||||
tunnelEnabled ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" : "bg-sidebar text-text-muted"
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
||||
tunnelEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
||||
}`}>Tunnel</span>
|
||||
{tunnelEnabled && !tunnelLoading ? (
|
||||
<>
|
||||
@@ -742,7 +742,6 @@ export default function APIPageClient({ machineId }) {
|
||||
}
|
||||
setShowEnableTunnelModal(true);
|
||||
}}
|
||||
className="bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 text-white!"
|
||||
>
|
||||
Enable
|
||||
</Button>
|
||||
@@ -750,8 +749,8 @@ export default function APIPageClient({ machineId }) {
|
||||
</div>
|
||||
{/* Tailscale */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[68px] text-center ${
|
||||
tsEnabled ? "bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" : "bg-sidebar text-text-muted"
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
||||
tsEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
||||
}`}>Tailscale</span>
|
||||
{tsEnabled && !tsLoading ? (
|
||||
<>
|
||||
@@ -850,8 +849,8 @@ export default function APIPageClient({ machineId }) {
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-lg font-semibold">Token Saver</h2>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 pb-4 border-b border-border">
|
||||
<div className="pr-4">
|
||||
<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
|
||||
@@ -864,7 +863,7 @@ export default function APIPageClient({ machineId }) {
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
Auto-compress tool output (git diff/grep/ls/tree/logs) before sending to LLM (60-90% fewer input tokens on common dev commands). Disable if you see issues.
|
||||
git/grep/ls/tree/logs → 60-90% fewer input tokens
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
@@ -872,8 +871,8 @@ export default function APIPageClient({ machineId }) {
|
||||
onChange={() => handleRtkEnabled(!rtkEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div className="pr-4">
|
||||
<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
|
||||
@@ -886,32 +885,34 @@ export default function APIPageClient({ machineId }) {
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
Inject a terse-style instruction into the system prompt so the LLM replies shorter (~65% fewer output tokens on average, up to 87%). Code, errors and warnings stay exact.
|
||||
Terse-style system prompt → ~65% fewer output tokens (up to 87%)
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={cavemanEnabled}
|
||||
onChange={() => handleCavemanEnabled(!cavemanEnabled)}
|
||||
/>
|
||||
</div>
|
||||
{cavemanEnabled && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
{CAVEMAN_LEVELS.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-black/5 dark:hover:bg-white/5"
|
||||
}`}
|
||||
title={lvl.desc}
|
||||
>
|
||||
{lvl.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
{cavemanEnabled && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{CAVEMAN_LEVELS.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>
|
||||
)}
|
||||
<Toggle
|
||||
checked={cavemanEnabled}
|
||||
onChange={() => handleCavemanEnabled(!cavemanEnabled)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* API Keys */}
|
||||
@@ -1089,14 +1090,14 @@ export default function APIPageClient({ machineId }) {
|
||||
onClose={() => setShowEnableTunnelModal(false)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="bg-surface-2 border border-border-subtle rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400">cloud_upload</span>
|
||||
<span className="material-symbols-outlined text-primary">cloud_upload</span>
|
||||
<div>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 font-medium mb-1">
|
||||
<p className="text-sm text-text-main font-medium mb-1">
|
||||
Cloudflare Tunnel
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
<p className="text-sm text-text-muted">
|
||||
Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1118,11 +1119,7 @@ export default function APIPageClient({ machineId }) {
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleEnableTunnel}
|
||||
fullWidth
|
||||
className="bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 text-white!"
|
||||
>
|
||||
<Button onClick={handleEnableTunnel} fullWidth>
|
||||
Start Tunnel
|
||||
</Button>
|
||||
<Button onClick={() => setShowEnableTunnelModal(false)} variant="ghost" fullWidth>Cancel</Button>
|
||||
@@ -1139,7 +1136,7 @@ export default function APIPageClient({ machineId }) {
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-text-muted">The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleDisableTunnel} fullWidth disabled={tunnelLoading} className="bg-red-500! hover:bg-red-600! text-white!">
|
||||
<Button onClick={handleDisableTunnel} fullWidth disabled={tunnelLoading} variant="danger">
|
||||
{tunnelLoading ? "Disabling..." : "Disable"}
|
||||
</Button>
|
||||
<Button onClick={() => setShowDisableTunnelModal(false)} variant="ghost" fullWidth disabled={tunnelLoading}>Cancel</Button>
|
||||
@@ -1167,11 +1164,7 @@ export default function APIPageClient({ machineId }) {
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-text-muted">Tailscale is not installed. Install it to enable Funnel.</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleInstallTailscale}
|
||||
fullWidth
|
||||
className="bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!"
|
||||
>
|
||||
<Button onClick={handleInstallTailscale} fullWidth>
|
||||
Install Tailscale
|
||||
</Button>
|
||||
<Button onClick={() => setShowTsModal(false)} variant="ghost" fullWidth>Cancel</Button>
|
||||
@@ -1211,7 +1204,6 @@ export default function APIPageClient({ machineId }) {
|
||||
handleConnectTailscale(tab);
|
||||
}}
|
||||
fullWidth
|
||||
className="bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!"
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
@@ -1233,7 +1225,7 @@ export default function APIPageClient({ machineId }) {
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-text-muted">Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleDisableTailscale} fullWidth disabled={tsLoading} className="bg-red-500! hover:bg-red-600! text-white!">
|
||||
<Button onClick={handleDisableTailscale} fullWidth disabled={tsLoading} variant="danger">
|
||||
{tsLoading ? "Disabling..." : "Disable"}
|
||||
</Button>
|
||||
<Button onClick={() => setShowDisableTsModal(false)} variant="ghost" fullWidth disabled={tsLoading}>Cancel</Button>
|
||||
@@ -1248,9 +1240,8 @@ export default function APIPageClient({ machineId }) {
|
||||
function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[68px] text-center ${badge === "CF" ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" :
|
||||
badge === "TS" ? "bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" :
|
||||
"bg-sidebar text-text-muted"
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
|
||||
(badge === "CF" || badge === "TS") ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
|
||||
}`}>{label}</span>
|
||||
<Input value={url} readOnly className="flex-1 font-mono text-sm" />
|
||||
<button
|
||||
|
||||
@@ -75,11 +75,7 @@ export default function MitmPageClient() {
|
||||
const mitmTools = Object.entries(MITM_TOOLS);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-1 sm:px-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-xl font-semibold text-text-main sm:text-2xl">MITM</h1>
|
||||
<p className="text-sm text-text-muted">Route supported IDE traffic through 9Router with local DNS interception.</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* MITM Server Card */}
|
||||
<MitmServerCard
|
||||
apiKeys={apiKeys}
|
||||
|
||||
@@ -762,15 +762,15 @@ export default function ProviderDetailPage() {
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">{providerInfo.name}</h1>
|
||||
{providerInfo.notice?.apiKeyUrl && !providerInfo.deprecated && (
|
||||
{(providerInfo.notice?.apiKeyUrl || providerInfo.notice?.signupUrl || providerInfo.website) && (
|
||||
<a
|
||||
href={providerInfo.notice.apiKeyUrl}
|
||||
href={providerInfo.notice?.apiKeyUrl || providerInfo.notice?.signupUrl || providerInfo.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">open_in_new</span>
|
||||
Get API Key
|
||||
{providerInfo.notice?.apiKeyUrl ? "Get API Key" : "Sign up / Learn more"}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -911,20 +911,21 @@ export default function ProviderDetailPage() {
|
||||
</div>
|
||||
|
||||
{connections.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||
<span className="material-symbols-outlined text-[32px]">{isOAuth ? "lock" : "key"}</span>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="inline-flex items-center justify-center w-9 h-9 rounded-full bg-primary/10 text-primary shrink-0">
|
||||
<span className="material-symbols-outlined text-[18px]">{isOAuth ? "lock" : "key"}</span>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">No connections yet</p>
|
||||
</div>
|
||||
<p className="text-text-main font-medium mb-1">No connections yet</p>
|
||||
<p className="text-sm text-text-muted mb-4">Add your first connection to get started</p>
|
||||
{!isCompatible && (
|
||||
<div className="flex flex-col gap-2 justify-center sm:flex-row">
|
||||
<div className="flex gap-2">
|
||||
{providerId === "iflow" && (
|
||||
<Button icon="cookie" variant="secondary" onClick={() => setShowIFlowCookieModal(true)}>
|
||||
Cookie Auth
|
||||
<Button size="sm" icon="cookie" variant="secondary" onClick={() => setShowIFlowCookieModal(true)}>
|
||||
Cookie
|
||||
</Button>
|
||||
)}
|
||||
<Button icon="add" onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
|
||||
<Button size="sm" icon="add" onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
|
||||
{providerId === "iflow" ? "OAuth" : "Add Connection"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,13 @@ import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
import RequestDetailsTab from "./components/RequestDetailsTab";
|
||||
|
||||
const PERIODS = [
|
||||
{ value: "24h", label: "24h" },
|
||||
{ value: "7d", label: "7D" },
|
||||
{ value: "30d", label: "30D" },
|
||||
{ value: "60d", label: "60D" },
|
||||
];
|
||||
|
||||
export default function UsagePage() {
|
||||
return (
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
@@ -18,6 +25,7 @@ function UsageContent() {
|
||||
const router = useRouter();
|
||||
|
||||
const [tabLoading, setTabLoading] = useState(false);
|
||||
const [period, setPeriod] = useState("7d");
|
||||
|
||||
const tabFromUrl = searchParams.get("tab");
|
||||
const activeTab = tabFromUrl && ["overview", "logs", "details"].includes(tabFromUrl)
|
||||
@@ -30,21 +38,32 @@ function UsageContent() {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("tab", value);
|
||||
router.push(`/dashboard/usage?${params.toString()}`, { scroll: false });
|
||||
// Brief loading flash so user sees feedback
|
||||
setTimeout(() => setTabLoading(false), 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ value: "overview", label: "Overview" },
|
||||
{ value: "details", label: "Details" },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
{/* Tabs + period selector on same row */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ value: "overview", label: "Overview" },
|
||||
{ value: "details", label: "Details" },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
{activeTab === "overview" && (
|
||||
<SegmentedControl
|
||||
options={PERIODS}
|
||||
value={period}
|
||||
onChange={setPeriod}
|
||||
size="sm"
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tabLoading ? (
|
||||
<CardSkeleton />
|
||||
@@ -52,7 +71,7 @@ function UsageContent() {
|
||||
<>
|
||||
{activeTab === "overview" && (
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<UsageStats />
|
||||
<UsageStats period={period} setPeriod={setPeriod} hidePeriodSelector />
|
||||
</Suspense>
|
||||
)}
|
||||
{activeTab === "logs" && <RequestLogger />}
|
||||
@@ -62,4 +81,3 @@ function UsageContent() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user