mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
chore(cli): remove unused BaseUrlSelect/ApiKeySelect/ManualConfigModal (plan 14)
Components were created by F4 per master-plan §3.7-§3.9 but never integrated: the `*ToolCard.tsx` files use the legacy `ManualConfigModal` from `@/shared/components` (barrel-export root), not the F4 versions in `@/shared/components/cli`. The 3 files were sitting as dead code. Removes: - src/shared/components/cli/BaseUrlSelect.tsx - src/shared/components/cli/ApiKeySelect.tsx - src/shared/components/cli/ManualConfigModal.tsx - tests/unit/ui/BaseUrlSelect.test.tsx - tests/unit/ui/ApiKeySelect.test.tsx - tests/unit/ui/ManualConfigModal.test.tsx Updates `src/shared/components/cli/index.ts` to drop the dead exports. Kept (actively used by page clients): - CliToolCard, CliConceptCard, CliComparisonCard Verified: - typecheck:core + noimplicit:core clean - npx eslint src/shared/components/cli/: 0 issues - check:cycles: clean (212 files, -3 from removed components) - 50/50 UI tests pass across the 3 kept components + 3 page clients - 0 residual imports of the 3 removed symbols anywhere in src/ or tests/ Closes code review v3 gap #1 (dead code).
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
export interface ApiKeyEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
prefix?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ApiKeySelectProps {
|
||||
keys: ApiKeyEntry[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MANUAL_VALUE = "__manual__";
|
||||
|
||||
export default function ApiKeySelect({
|
||||
keys,
|
||||
value,
|
||||
onChange,
|
||||
label = "API Key",
|
||||
disabled = false,
|
||||
}: ApiKeySelectProps) {
|
||||
const isManual = !keys.some((k) => k.id === value) && value !== "";
|
||||
const [showManual, setShowManual] = useState<boolean>(isManual);
|
||||
const [manualValue, setManualValue] = useState<string>(isManual ? value : "");
|
||||
|
||||
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const val = e.target.value;
|
||||
if (val === MANUAL_VALUE) {
|
||||
setShowManual(true);
|
||||
onChange(manualValue);
|
||||
} else {
|
||||
setShowManual(false);
|
||||
onChange(val);
|
||||
}
|
||||
}
|
||||
|
||||
function handleManualInput(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const val = e.target.value;
|
||||
setManualValue(val);
|
||||
onChange(val);
|
||||
}
|
||||
|
||||
const selectValue = showManual ? MANUAL_VALUE : (value || "");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{label && (
|
||||
<label className="text-sm font-medium text-text-main">{label}</label>
|
||||
)}
|
||||
<select
|
||||
value={selectValue}
|
||||
onChange={handleSelectChange}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 rounded-lg text-sm",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
|
||||
"text-text-main focus:outline-none focus:border-primary/50",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{keys.length === 0 && !showManual && (
|
||||
<option value="" disabled>
|
||||
Nenhuma API key cadastrada
|
||||
</option>
|
||||
)}
|
||||
{keys.map((k) => (
|
||||
<option key={k.id} value={k.id}>
|
||||
{k.name}
|
||||
{k.prefix ? ` (${k.prefix}…)` : ""}
|
||||
</option>
|
||||
))}
|
||||
<option value={MANUAL_VALUE}>Inserir manualmente</option>
|
||||
</select>
|
||||
{showManual && (
|
||||
<input
|
||||
type="text"
|
||||
value={manualValue}
|
||||
onChange={handleManualInput}
|
||||
disabled={disabled}
|
||||
placeholder="sk-…"
|
||||
className={cn(
|
||||
"w-full px-3 py-2 rounded-lg text-sm font-mono",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
|
||||
"text-text-main focus:outline-none focus:border-primary/50",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
export interface BaseUrlSelectProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
cloudEnabled: boolean;
|
||||
cloudUrl?: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type OptionKey = "local" | "cloud" | "custom";
|
||||
|
||||
function getDefaultLocal(): string {
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.origin;
|
||||
}
|
||||
return "http://localhost:20128";
|
||||
}
|
||||
|
||||
export default function BaseUrlSelect({
|
||||
value,
|
||||
onChange,
|
||||
cloudEnabled,
|
||||
cloudUrl,
|
||||
label = "Base URL",
|
||||
disabled = false,
|
||||
}: BaseUrlSelectProps) {
|
||||
const localUrl = getDefaultLocal();
|
||||
|
||||
function detectOption(val: string): OptionKey {
|
||||
if (val === localUrl) return "local";
|
||||
if (cloudEnabled && cloudUrl && val === cloudUrl) return "cloud";
|
||||
return "custom";
|
||||
}
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<OptionKey>(() => detectOption(value));
|
||||
const [customValue, setCustomValue] = useState<string>(
|
||||
detectOption(value) === "custom" ? value : ""
|
||||
);
|
||||
|
||||
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const opt = e.target.value as OptionKey;
|
||||
setSelectedOption(opt);
|
||||
if (opt === "local") {
|
||||
onChange(localUrl);
|
||||
} else if (opt === "cloud" && cloudUrl) {
|
||||
onChange(cloudUrl);
|
||||
} else if (opt === "custom") {
|
||||
onChange(customValue);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomInput(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const val = e.target.value;
|
||||
setCustomValue(val);
|
||||
onChange(val);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{label && (
|
||||
<label className="text-sm font-medium text-text-main">{label}</label>
|
||||
)}
|
||||
<select
|
||||
value={selectedOption}
|
||||
onChange={handleSelectChange}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 rounded-lg text-sm",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
|
||||
"text-text-main focus:outline-none focus:border-primary/50",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<option value="local">Local ({localUrl})</option>
|
||||
{cloudEnabled && cloudUrl && (
|
||||
<option value="cloud">Cloud ({cloudUrl})</option>
|
||||
)}
|
||||
<option value="custom">Custom…</option>
|
||||
</select>
|
||||
{selectedOption === "custom" && (
|
||||
<input
|
||||
type="text"
|
||||
value={customValue}
|
||||
onChange={handleCustomInput}
|
||||
disabled={disabled}
|
||||
placeholder="https://…"
|
||||
className={cn(
|
||||
"w-full px-3 py-2 rounded-lg text-sm font-mono",
|
||||
"bg-white dark:bg-white/5 border border-black/10 dark:border-white/10",
|
||||
"text-text-main focus:outline-none focus:border-primary/50",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog";
|
||||
|
||||
export interface ManualConfigModalCustomCode {
|
||||
language: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface ManualConfigModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
tool: CliCatalogEntry;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
customCode?: ManualConfigModalCustomCode;
|
||||
}
|
||||
|
||||
function interpolate(template: string, vars: Record<string, string>): string {
|
||||
return template
|
||||
.replace(/\{\{baseUrl\}\}/g, vars["baseUrl"] ?? "")
|
||||
.replace(/\{\{apiKey\}\}/g, vars["apiKey"] ?? "")
|
||||
.replace(/\{\{model\}\}/g, vars["model"] ?? "");
|
||||
}
|
||||
|
||||
export default function ManualConfigModal({
|
||||
open,
|
||||
onClose,
|
||||
tool,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
model,
|
||||
customCode,
|
||||
}: ManualConfigModalProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const source = customCode ?? tool.codeBlock;
|
||||
const vars: Record<string, string> = { baseUrl, apiKey, model };
|
||||
const rendered = source ? interpolate(source.code, vars) : "";
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(rendered);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// fallback: do nothing — clipboard unavailable in non-secure context
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
/* Overlay */
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Configuração manual — ${tool.name}`}
|
||||
className={cn(
|
||||
"relative w-full bg-surface",
|
||||
"border border-black/10 dark:border-white/10",
|
||||
"rounded-xl shadow-2xl",
|
||||
// Desktop: centered, max-w; Mobile: full-screen
|
||||
"max-w-xl sm:max-w-2xl",
|
||||
"max-h-screen sm:max-h-[90vh] overflow-y-auto"
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-5 border-b border-black/5 dark:border-white/5">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-text-main">
|
||||
{tool.name} — Configuração Manual
|
||||
</h2>
|
||||
{source && (
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
Linguagem: <span className="font-mono">{source.language}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Fechar"
|
||||
className="p-1.5 rounded-lg text-text-muted hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-5 flex flex-col gap-4">
|
||||
{source ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-text-main">Código de configuração</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-all",
|
||||
copied
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
: "bg-black/5 dark:bg-white/5 text-text-muted hover:text-text-main hover:bg-black/10 dark:hover:bg-white/10"
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="px-4 py-3 bg-black/5 dark:bg-white/5 rounded-lg font-mono text-xs overflow-x-auto whitespace-pre-wrap break-all border border-black/5 dark:border-white/5 leading-relaxed">
|
||||
{rendered}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">
|
||||
Nenhum bloco de configuração disponível para {tool.name}.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Variable summary */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
|
||||
{(
|
||||
[
|
||||
{ key: "Base URL", val: baseUrl },
|
||||
{ key: "API Key", val: apiKey ? `${apiKey.slice(0, 8)}…` : "(não definida)" },
|
||||
{ key: "Model", val: model },
|
||||
] as Array<{ key: string; val: string }>
|
||||
).map(({ key, val }) => (
|
||||
<div key={key} className="flex flex-col gap-0.5">
|
||||
<span className="text-[10px] uppercase tracking-wider text-text-muted">{key}</span>
|
||||
<span className="text-xs font-mono text-text-main truncate">{val}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,3 @@ export type { CliConceptCardProps, CliConceptType } from "./CliConceptCard";
|
||||
|
||||
export { default as CliComparisonCard } from "./CliComparisonCard";
|
||||
export type { CliComparisonCardProps } from "./CliComparisonCard";
|
||||
|
||||
export { default as BaseUrlSelect } from "./BaseUrlSelect";
|
||||
export type { BaseUrlSelectProps } from "./BaseUrlSelect";
|
||||
|
||||
export { default as ApiKeySelect } from "./ApiKeySelect";
|
||||
export type { ApiKeySelectProps, ApiKeyEntry } from "./ApiKeySelect";
|
||||
|
||||
export { default as ManualConfigModal } from "./ManualConfigModal";
|
||||
export type { ManualConfigModalProps, ManualConfigModalCustomCode } from "./ManualConfigModal";
|
||||
|
||||
Reference in New Issue
Block a user