Enhance image and embedding provider support
- Added new image models for GPT 5.2, 5.3, and 5.4, including capabilities for text-to-image and editing. - Updated embedding handling to include optional dimensions in requests. - Introduced support for custom embedding providers, allowing dynamic fetching and validation of custom nodes. - Improved image generation handling with Codex integration, including progress tracking and error handling. - Enhanced UI components to support adding custom embeddings and displaying their status.
This commit is contained in:
parent
cca615eaff
commit
0b8bed5793
19 changed files with 1039 additions and 130 deletions
183
src/shared/components/AddCustomEmbeddingModal.js
Normal file
183
src/shared/components/AddCustomEmbeddingModal.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"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,
|
||||
}),
|
||||
};
|
||||
|
|
@ -29,6 +29,7 @@ export { default as CursorAuthModal } from "./CursorAuthModal";
|
|||
export { default as IFlowCookieModal } from "./IFlowCookieModal";
|
||||
export { default as GitLabAuthModal } from "./GitLabAuthModal";
|
||||
export { default as EditConnectionModal } from "./EditConnectionModal";
|
||||
export { default as AddCustomEmbeddingModal } from "./AddCustomEmbeddingModal";
|
||||
export { default as SegmentedControl } from "./SegmentedControl";
|
||||
export { default as Tooltip } from "./Tooltip";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue