Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useState, useEffect } from "react"; | |
| import PropTypes from "prop-types"; | |
| import { Modal, Input, Button, Badge } from "@/shared/components"; | |
| const DEFAULT_BASE_URL = "https://api.openai.com/v1"; | |
| // Dual-mode modal: edit when `node` provided, add otherwise | |
| export default function AddCustomEmbeddingModal({ isOpen, onClose, onCreated, onSaved, node }) { | |
| const isEdit = !!node; | |
| const [formData, setFormData] = useState({ | |
| name: "", | |
| prefix: "", | |
| baseUrl: DEFAULT_BASE_URL, | |
| }); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [checkKey, setCheckKey] = useState(""); | |
| const [checkModelId, setCheckModelId] = useState(""); | |
| const [validating, setValidating] = useState(false); | |
| const [validationResult, setValidationResult] = useState(null); | |
| useEffect(() => { | |
| if (!isOpen) return; | |
| setValidationResult(null); | |
| setCheckKey(""); | |
| setCheckModelId(""); | |
| if (isEdit) { | |
| setFormData({ | |
| name: node.name || "", | |
| prefix: node.prefix || "", | |
| baseUrl: node.baseUrl || DEFAULT_BASE_URL, | |
| }); | |
| } else { | |
| setFormData({ name: "", prefix: "", baseUrl: DEFAULT_BASE_URL }); | |
| } | |
| }, [isOpen, isEdit, node]); | |
| const handleSubmit = async () => { | |
| if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return; | |
| setSubmitting(true); | |
| try { | |
| const url = isEdit ? `/api/provider-nodes/${node.id}` : "/api/provider-nodes"; | |
| const method = isEdit ? "PUT" : "POST"; | |
| const payload = { | |
| name: formData.name, | |
| prefix: formData.prefix, | |
| baseUrl: formData.baseUrl, | |
| }; | |
| if (!isEdit) payload.type = "custom-embedding"; | |
| const res = await fetch(url, { | |
| method, | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| if (isEdit) onSaved?.(data.node); | |
| else onCreated?.(data.node); | |
| } | |
| } catch (error) { | |
| console.log("Error saving custom embedding node:", error); | |
| } finally { | |
| setSubmitting(false); | |
| } | |
| }; | |
| const handleValidate = async () => { | |
| setValidating(true); | |
| try { | |
| const res = await fetch("/api/provider-nodes/validate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| baseUrl: formData.baseUrl, | |
| apiKey: checkKey, | |
| type: "custom-embedding", | |
| modelId: checkModelId.trim() || undefined, | |
| }), | |
| }); | |
| const data = await res.json(); | |
| setValidationResult(data); | |
| } catch { | |
| setValidationResult({ valid: false, error: "Network error" }); | |
| } finally { | |
| setValidating(false); | |
| } | |
| }; | |
| const renderValidationResult = () => { | |
| if (!validationResult) return null; | |
| const { valid, error, dimensions } = validationResult; | |
| if (valid) { | |
| return ( | |
| <> | |
| <Badge variant="success">Valid</Badge> | |
| {dimensions && <span className="text-sm text-text-muted">{dimensions} dims</span>} | |
| </> | |
| ); | |
| } | |
| return ( | |
| <div className="flex flex-col gap-1"> | |
| <Badge variant="error">Invalid</Badge> | |
| {error && <span className="text-sm text-red-500">{error}</span>} | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <Modal isOpen={isOpen} title={isEdit ? "Edit Custom Embedding" : "Add Custom Embedding"} onClose={onClose}> | |
| <div className="flex flex-col gap-4"> | |
| <Input | |
| label="Name" | |
| value={formData.name} | |
| onChange={(e) => setFormData({ ...formData, name: e.target.value })} | |
| placeholder="Voyage AI" | |
| hint="Required. A friendly label for this embedding provider." | |
| /> | |
| <Input | |
| label="Prefix" | |
| value={formData.prefix} | |
| onChange={(e) => setFormData({ ...formData, prefix: e.target.value })} | |
| placeholder="voyage" | |
| hint="Required. Used as the provider prefix for model IDs (e.g. voyage/voyage-3)." | |
| /> | |
| <Input | |
| label="Base URL" | |
| value={formData.baseUrl} | |
| onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })} | |
| placeholder="https://api.voyageai.com/v1" | |
| hint="Most embedding APIs are OpenAI-compatible: Voyage, Cohere, Jina, Mistral, Together..." | |
| /> | |
| <Input | |
| label="API Key (for Check)" | |
| type="password" | |
| value={checkKey} | |
| onChange={(e) => setCheckKey(e.target.value)} | |
| /> | |
| <Input | |
| label="Model ID (for Check)" | |
| value={checkModelId} | |
| onChange={(e) => setCheckModelId(e.target.value)} | |
| placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small" | |
| hint="Required for validation. Will send a test embeddings request." | |
| /> | |
| <div className="flex items-center gap-3"> | |
| <Button | |
| onClick={handleValidate} | |
| disabled={!checkKey || !checkModelId.trim() || validating || !formData.baseUrl.trim()} | |
| variant="secondary" | |
| > | |
| {validating ? "Checking..." : "Check"} | |
| </Button> | |
| {renderValidationResult()} | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button | |
| onClick={handleSubmit} | |
| fullWidth | |
| disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || submitting} | |
| > | |
| {submitting ? (isEdit ? "Saving..." : "Creating...") : (isEdit ? "Save" : "Create")} | |
| </Button> | |
| <Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button> | |
| </div> | |
| </div> | |
| </Modal> | |
| ); | |
| } | |
| AddCustomEmbeddingModal.propTypes = { | |
| isOpen: PropTypes.bool.isRequired, | |
| onClose: PropTypes.func.isRequired, | |
| onCreated: PropTypes.func, | |
| onSaved: PropTypes.func, | |
| node: PropTypes.shape({ | |
| id: PropTypes.string, | |
| name: PropTypes.string, | |
| prefix: PropTypes.string, | |
| baseUrl: PropTypes.string, | |
| }), | |
| }; | |