Spaces:
Runtime error
Runtime error
| "use client"; | |
| import { useEffect, useMemo, useState } from "react"; | |
| import { useTranslations } from "next-intl"; | |
| import { SegmentedControl, Collapsible } from "@/shared/components"; | |
| import RtkLearnDiscoverCard from "./RtkLearnDiscoverCard"; | |
| type RtkFilter = { | |
| id: string; | |
| name: string; | |
| description: string; | |
| commandTypes: string[]; | |
| category: string; | |
| priority: number; | |
| }; | |
| type RtkConfig = { | |
| enabled: boolean; | |
| intensity: "minimal" | "standard" | "aggressive"; | |
| applyToToolResults: boolean; | |
| applyToAssistantMessages: boolean; | |
| applyToCodeBlocks: boolean; | |
| enabledFilters: string[]; | |
| disabledFilters: string[]; | |
| maxLinesPerResult: number; | |
| maxCharsPerResult: number; | |
| deduplicateThreshold: number; | |
| customFiltersEnabled: boolean; | |
| trustProjectFilters: boolean; | |
| rawOutputRetention: "never" | "failures" | "always"; | |
| rawOutputMaxBytes: number; | |
| }; | |
| type AnalyticsSummary = { | |
| totalRequests: number; | |
| totalTokensSaved: number; | |
| avgSavingsPct: number; | |
| byEngine?: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }>; | |
| }; | |
| type PreviewResult = { | |
| text?: string; | |
| compressed?: boolean; | |
| originalTokens?: number; | |
| compressedTokens?: number; | |
| techniquesUsed?: string[]; | |
| detection?: { type: string; confidence: number; category: string }; | |
| error?: string; | |
| }; | |
| const SAMPLE_OUTPUT = `$ npm run typecheck | |
| src/lib/example.ts:10:15 - error TS2322: Type 'string' is not assignable to type 'number'. | |
| 10 const value: number = "bad"; | |
| ~~~~~ | |
| Found 1 error in src/lib/example.ts:10`; | |
| function formatNumber(value: number | undefined): string { | |
| return new Intl.NumberFormat().format(value ?? 0); | |
| } | |
| export default function RtkContextPageClient() { | |
| const t = useTranslations("contextRtk"); | |
| const [filters, setFilters] = useState<RtkFilter[]>([]); | |
| const [config, setConfig] = useState<RtkConfig | null>(null); | |
| const [analytics, setAnalytics] = useState<AnalyticsSummary | null>(null); | |
| const [sample, setSample] = useState(SAMPLE_OUTPUT); | |
| const [preview, setPreview] = useState<PreviewResult | null>(null); | |
| const [saving, setSaving] = useState(false); | |
| const [viewMode, setViewMode] = useState<"simple" | "advanced">("simple"); | |
| const [masterEnabled, setMasterEnabled] = useState<boolean | null>(null); | |
| useEffect(() => { | |
| fetch("/api/settings/compression") | |
| .then((res) => (res.ok ? res.json() : null)) | |
| .then((data) => setMasterEnabled(data?.enabled ?? false)) | |
| .catch(() => {}); | |
| }, []); | |
| useEffect(() => { | |
| fetch("/api/context/rtk/filters") | |
| .then((res) => (res.ok ? res.json() : null)) | |
| .then((data) => setFilters(Array.isArray(data?.filters) ? data.filters : [])) | |
| .catch(() => {}); | |
| fetch("/api/context/rtk/config") | |
| .then((res) => (res.ok ? res.json() : null)) | |
| .then((data) => setConfig(data)) | |
| .catch(() => {}); | |
| fetch("/api/context/analytics?since=7d") | |
| .then((res) => (res.ok ? res.json() : null)) | |
| .then((data) => setAnalytics(data)) | |
| .catch(() => {}); | |
| }, []); | |
| const groupedFilters = useMemo(() => { | |
| return filters.reduce<Record<string, RtkFilter[]>>((groups, filter) => { | |
| groups[filter.category] = [...(groups[filter.category] ?? []), filter]; | |
| return groups; | |
| }, {}); | |
| }, [filters]); | |
| const activeFilterCount = useMemo(() => { | |
| if (!config) return 0; | |
| if (config.enabledFilters.length > 0) return config.enabledFilters.length; | |
| return filters.filter((filter) => !config.disabledFilters.includes(filter.id)).length; | |
| }, [config, filters]); | |
| const saveConfig = async (patch: Partial<RtkConfig>) => { | |
| if (!config) return; | |
| const nextConfig = { ...config, ...patch }; | |
| setConfig(nextConfig); | |
| setSaving(true); | |
| try { | |
| const res = await fetch("/api/context/rtk/config", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(patch), | |
| }); | |
| if (res.ok) setConfig(await res.json()); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const toggleFilter = (filterId: string, enabled: boolean) => { | |
| if (!config) return; | |
| const disabledFilters = enabled | |
| ? config.disabledFilters.filter((id) => id !== filterId) | |
| : [...new Set([...config.disabledFilters, filterId])]; | |
| saveConfig({ disabledFilters }); | |
| }; | |
| const runPreview = async () => { | |
| const res = await fetch("/api/context/rtk/test", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ text: sample, config: config ?? undefined }), | |
| }); | |
| setPreview(res.ok ? await res.json() : { error: await res.text() }); | |
| }; | |
| const rtkStats = analytics?.byEngine?.rtk; | |
| const statCards = [ | |
| [t("tokensFiltered"), formatNumber(rtkStats?.tokensSaved ?? analytics?.totalTokensSaved)], | |
| [t("filtersActive"), formatNumber(activeFilterCount)], | |
| [t("requests"), formatNumber(rtkStats?.count ?? analytics?.totalRequests)], | |
| [t("avgSavings"), `${rtkStats?.avgSavingsPct ?? analytics?.avgSavingsPct ?? 0}%`], | |
| ]; | |
| return ( | |
| <div className="mx-auto flex max-w-6xl flex-col gap-6"> | |
| <header className="flex flex-col gap-2"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <div className="flex items-center gap-3"> | |
| <span className="material-symbols-outlined text-[30px] text-primary">filter_alt</span> | |
| <div> | |
| <h1 className="text-2xl font-bold text-text-main">{t("title")}</h1> | |
| <p className="text-sm text-text-muted">{t("description")}</p> | |
| </div> | |
| </div> | |
| <SegmentedControl | |
| value={viewMode} | |
| onChange={(v) => setViewMode(v as "simple" | "advanced")} | |
| options={[ | |
| { value: "simple", label: t("simpleMode") || "Simple" }, | |
| { value: "advanced", label: t("advancedMode") || "Advanced" }, | |
| ]} | |
| /> | |
| </div> | |
| </header> | |
| <section className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4"> | |
| {statCards.map(([label, value]) => ( | |
| <div key={label} className="rounded-lg border border-border bg-surface p-4"> | |
| <p className="text-xs uppercase text-text-muted">{label}</p> | |
| <p className="mt-1 text-xl font-semibold text-text-main">{value}</p> | |
| </div> | |
| ))} | |
| </section> | |
| {masterEnabled === false && ( | |
| <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-700 dark:text-amber-300 flex items-start gap-2"> | |
| <span className="material-symbols-outlined text-[18px]">info</span> | |
| <p>{t("masterSwitchOffAlert")}</p> | |
| </div> | |
| )} | |
| {config && ( | |
| <section className="rounded-lg border border-border bg-surface p-4"> | |
| {/* On/off + intensity now live in the panel (/dashboard/context/settings). This | |
| page edits RTK's detailed configuration only. */} | |
| <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"> | |
| <label className="flex flex-col gap-1 text-sm text-text-main"> | |
| {t("maxLines")} | |
| <input | |
| type="number" | |
| min={0} | |
| value={config.maxLinesPerResult} | |
| onChange={(event) => | |
| saveConfig({ maxLinesPerResult: Number(event.target.value) || 0 }) | |
| } | |
| className="rounded border border-border bg-bg px-2 py-1 text-sm" | |
| /> | |
| </label> | |
| <label className="flex flex-col gap-1 text-sm text-text-main"> | |
| {t("maxChars")} | |
| <input | |
| type="number" | |
| min={0} | |
| value={config.maxCharsPerResult} | |
| onChange={(event) => | |
| saveConfig({ maxCharsPerResult: Number(event.target.value) || 0 }) | |
| } | |
| className="rounded border border-border bg-bg px-2 py-1 text-sm" | |
| /> | |
| </label> | |
| <label className="flex flex-col gap-1 text-sm text-text-main"> | |
| {t("deduplicateThreshold")} | |
| <input | |
| type="number" | |
| min={2} | |
| max={100} | |
| value={config.deduplicateThreshold} | |
| onChange={(event) => | |
| saveConfig({ deduplicateThreshold: Number(event.target.value) || 2 }) | |
| } | |
| className="rounded border border-border bg-bg px-2 py-1 text-sm" | |
| /> | |
| </label> | |
| <label className="flex flex-col gap-1 text-sm text-text-main"> | |
| {t("rawOutputMaxBytes")} | |
| <input | |
| type="number" | |
| min={1024} | |
| value={config.rawOutputMaxBytes} | |
| onChange={(event) => | |
| saveConfig({ rawOutputMaxBytes: Number(event.target.value) || 1024 }) | |
| } | |
| className="rounded border border-border bg-bg px-2 py-1 text-sm" | |
| /> | |
| </label> | |
| </div> | |
| <div className="mt-4 flex flex-wrap gap-4 text-sm text-text-main"> | |
| {[ | |
| ["applyToToolResults", t("toolResults")], | |
| ["applyToAssistantMessages", t("assistantMessages")], | |
| ["applyToCodeBlocks", t("codeBlocks")], | |
| ["customFiltersEnabled", t("customFilters")], | |
| ["trustProjectFilters", t("trustProjectFilters")], | |
| ].map(([key, label]) => ( | |
| <label key={key} className="flex items-center gap-2"> | |
| <input | |
| type="checkbox" | |
| checked={Boolean(config[key as keyof RtkConfig])} | |
| disabled={saving} | |
| onChange={(event) => | |
| saveConfig({ [key]: event.target.checked } as Partial<RtkConfig>) | |
| } | |
| /> | |
| {label} | |
| </label> | |
| ))} | |
| </div> | |
| <div className="mt-4 max-w-sm text-sm text-text-main"> | |
| <label className="flex flex-col gap-1"> | |
| {t("rawOutputRetention")} | |
| <select | |
| value={config.rawOutputRetention} | |
| disabled={saving} | |
| onChange={(event) => | |
| saveConfig({ | |
| rawOutputRetention: event.target.value as RtkConfig["rawOutputRetention"], | |
| }) | |
| } | |
| className="rounded border border-border bg-bg px-2 py-1 text-sm" | |
| > | |
| <option value="never">{t("rawOutputNever")}</option> | |
| <option value="failures">{t("rawOutputFailures")}</option> | |
| <option value="always">{t("rawOutputAlways")}</option> | |
| </select> | |
| </label> | |
| </div> | |
| </section> | |
| )} | |
| {viewMode === "advanced" && ( | |
| <section className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr]"> | |
| <div className="rounded-lg border border-border bg-surface p-4"> | |
| <div className="mb-3 flex items-center justify-between"> | |
| <h2 className="text-sm font-semibold text-text-main">{t("filterTesting")}</h2> | |
| <button | |
| onClick={runPreview} | |
| className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white" | |
| > | |
| {t("run")} | |
| </button> | |
| </div> | |
| <textarea | |
| value={sample} | |
| onChange={(event) => setSample(event.target.value)} | |
| placeholder={t("pasteOutput")} | |
| className="h-72 w-full rounded-lg border border-border bg-bg p-3 font-mono text-xs text-text-main" | |
| /> | |
| </div> | |
| <div className="rounded-lg border border-border bg-surface p-4"> | |
| <h2 className="mb-3 text-sm font-semibold text-text-main">{t("result")}</h2> | |
| {preview?.detection && ( | |
| <p className="mb-2 text-xs text-text-muted"> | |
| {t("detected")}: {preview.detection.type} ( | |
| {Math.round(preview.detection.confidence * 100)}%) | |
| </p> | |
| )} | |
| <pre className="h-72 overflow-auto rounded-lg border border-border bg-bg p-3 text-xs text-text-main"> | |
| {preview ? JSON.stringify(preview, null, 2) : t("previewEmpty")} | |
| </pre> | |
| </div> | |
| </section> | |
| )} | |
| <Collapsible | |
| title={t("filterCatalog") || "Filter Catalog"} | |
| subtitle={t("filterCatalogDesc") || "Available output filters by category"} | |
| icon="filter_list" | |
| trailing={ | |
| <span className="text-xs text-text-muted"> | |
| {Object.values(groupedFilters).flat().length} {t("filtersActive") || "filters"} | |
| </span> | |
| } | |
| defaultOpen={viewMode === "advanced"} | |
| > | |
| <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3"> | |
| {Object.entries(groupedFilters).map(([category, items]) => ( | |
| <div key={category} className="rounded-lg border border-border bg-bg p-3"> | |
| <h3 className="text-xs font-semibold capitalize text-text-main">{category}</h3> | |
| <div className="mt-2 space-y-2"> | |
| {items.map((filter) => { | |
| const enabled = config ? !config.disabledFilters.includes(filter.id) : true; | |
| return ( | |
| <div | |
| key={filter.id} | |
| className="border-t border-border pt-2 first:border-t-0 first:pt-0" | |
| > | |
| <div className="flex items-start justify-between gap-2"> | |
| <div className="min-w-0"> | |
| <p className="text-xs font-medium text-text-main">{filter.name}</p> | |
| <p className="mt-0.5 text-[11px] text-text-muted">{filter.description}</p> | |
| </div> | |
| <input | |
| type="checkbox" | |
| checked={enabled} | |
| disabled={!config || saving} | |
| onChange={(event) => toggleFilter(filter.id, event.target.checked)} | |
| className="mt-0.5" | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </Collapsible> | |
| <RtkLearnDiscoverCard /> | |
| </div> | |
| ); | |
| } | |