Files
9router/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/components/EmbeddingExampleCard.js
T

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">&#9889; {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>
);
}