mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
236 lines
9.8 KiB
JavaScript
236 lines
9.8 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Card } from "@/shared/components";
|
|
import { getProviderAlias, isCustomEmbeddingProvider } from "@/shared/constants/providers";
|
|
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
|
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
|
import { Row } from "./exampleShared";
|
|
|
|
const DEFAULT_RESPONSE_EXAMPLE = `{
|
|
"object": "list",
|
|
"data": [{
|
|
"object": "embedding",
|
|
"index": 0,
|
|
"embedding": [0.002301, -0.019212, 0.004815, -0.031249, ...]
|
|
}],
|
|
"model": "...",
|
|
"usage": { "prompt_tokens": 9, "total_tokens": 9 }
|
|
}`;
|
|
|
|
export function EmbeddingExampleCard({ providerId, customAlias }) {
|
|
const isCustom = isCustomEmbeddingProvider(providerId);
|
|
const providerAlias = isCustom ? (customAlias || providerId) : getProviderAlias(providerId);
|
|
const embeddingModels = isCustom ? [] : getModelsByProviderId(providerId).filter((m) => getModelKind(m) === "embedding");
|
|
|
|
const [selectedModel, setSelectedModel] = useState(embeddingModels[0]?.id ?? "");
|
|
const [input, setInput] = useState("The quick brown fox jumps over the lazy dog");
|
|
const [dimensions, setDimensions] = useState("");
|
|
const [apiKey, setApiKey] = useState("");
|
|
const [localEndpoint, setLocalEndpoint] = useState("");
|
|
const [result, setResult] = useState(null);
|
|
const [running, setRunning] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard();
|
|
const { copied: copiedRes, copy: copyRes } = useCopyToClipboard();
|
|
|
|
useEffect(() => {
|
|
setLocalEndpoint(window.location.origin);
|
|
fetch("/api/keys")
|
|
.then((r) => r.json())
|
|
.then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); })
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const endpoint = localEndpoint;
|
|
const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : "";
|
|
|
|
// Build request body — include dimensions only if user provided a positive number
|
|
const buildBody = () => {
|
|
const body = { model: modelFull, input: input.trim() };
|
|
const dim = Number(dimensions);
|
|
if (dimensions && Number.isFinite(dim) && dim > 0) body.dimensions = dim;
|
|
return body;
|
|
};
|
|
|
|
const curlSnippet = `curl -X POST ${endpoint}/v1/embeddings \\
|
|
-H "Content-Type: application/json" \\
|
|
-H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\
|
|
-d '${JSON.stringify(buildBody())}'`;
|
|
|
|
const handleRun = async () => {
|
|
if (!input.trim() || !modelFull) return;
|
|
setRunning(true);
|
|
setError("");
|
|
setResult(null);
|
|
const start = Date.now();
|
|
try {
|
|
const headers = { "Content-Type": "application/json" };
|
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
const res = await fetch("/api/v1/embeddings", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(buildBody()),
|
|
});
|
|
const latencyMs = Date.now() - start;
|
|
const data = await res.json();
|
|
if (!res.ok) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; }
|
|
setResult({ data, latencyMs });
|
|
} catch (e) {
|
|
setError(e.message || "Network error");
|
|
} finally {
|
|
setRunning(false);
|
|
}
|
|
};
|
|
|
|
// Compact embedding array: first 4 values + count
|
|
const formatResultJson = (data) => {
|
|
if (!data) return DEFAULT_RESPONSE_EXAMPLE;
|
|
const clone = JSON.parse(JSON.stringify(data));
|
|
(clone.data || []).forEach((item) => {
|
|
if (Array.isArray(item.embedding) && item.embedding.length > 4) {
|
|
item.embedding = [...item.embedding.slice(0, 4).map((v) => parseFloat(v.toFixed(6))), `... (${item.embedding.length} dims)`];
|
|
}
|
|
});
|
|
return JSON.stringify(clone, null, 2);
|
|
};
|
|
|
|
const resultJson = result ? JSON.stringify(result.data, null, 2) : "";
|
|
|
|
return (
|
|
<Card>
|
|
<h2 className="text-lg font-semibold mb-4">Example</h2>
|
|
|
|
<div className="flex flex-col gap-2.5">
|
|
{/* Model — text input for custom node, dropdown otherwise */}
|
|
<Row label="Model">
|
|
{isCustom ? (
|
|
<input
|
|
value={selectedModel}
|
|
onChange={(e) => setSelectedModel(e.target.value)}
|
|
placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small"
|
|
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
|
|
/>
|
|
) : (
|
|
<select
|
|
value={selectedModel}
|
|
onChange={(e) => setSelectedModel(e.target.value)}
|
|
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
|
>
|
|
{embeddingModels.map((m) => (
|
|
<option key={m.id} value={m.id}>{m.name || m.id}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</Row>
|
|
|
|
{/* Endpoint */}
|
|
<Row label="Endpoint">
|
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
|
|
<input
|
|
value={endpoint}
|
|
onChange={(e) => setLocalEndpoint(e.target.value)}
|
|
className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
|
|
placeholder="http://localhost:3000"
|
|
/>
|
|
</div>
|
|
</Row>
|
|
|
|
{/* API Key */}
|
|
<Row label="API Key">
|
|
<input
|
|
type="password"
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
placeholder="sk-..."
|
|
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
|
|
/>
|
|
</Row>
|
|
|
|
{/* Input */}
|
|
<Row label="Input">
|
|
<div className="relative">
|
|
<input
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
|
/>
|
|
{input && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setInput("")}
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[14px]">close</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</Row>
|
|
|
|
{/* Dimensions (optional) — truncate embedding vector length */}
|
|
<Row label="Dimensions">
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={dimensions}
|
|
onChange={(e) => setDimensions(e.target.value)}
|
|
placeholder="optional, e.g. 512, 1024 (leave empty for default)"
|
|
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
|
/>
|
|
</Row>
|
|
|
|
{/* Curl + Run */}
|
|
<div className="mt-1">
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
|
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
|
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
|
|
<button
|
|
onClick={() => copyCurl(curlSnippet)}
|
|
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
|
|
{copiedCurl ? "Copied" : "Copy"}
|
|
</button>
|
|
<button
|
|
onClick={handleRun}
|
|
disabled={running || !input.trim() || !modelFull}
|
|
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
|
|
play_arrow
|
|
</span>
|
|
{running ? "Running..." : "Run"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
|
|
|
|
{/* Response — default example or real result */}
|
|
<div>
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
|
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
|
|
Response {result && <span className="font-normal normal-case">⚡ {result.latencyMs}ms</span>}
|
|
</span>
|
|
{result && (
|
|
<button
|
|
onClick={() => copyRes(resultJson)}
|
|
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
|
|
{copiedRes ? "Copied" : "Copy"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-70">
|
|
{formatResultJson(result?.data)}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|