mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-30 21:59:46 +02:00
feat(model-connections): add test preview functionality for model connections
This commit is contained in:
parent
55f004e1da
commit
9f6210ad08
12 changed files with 294 additions and 77 deletions
|
|
@ -1,12 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { CheckCircle2, Trash2, XCircle } from "lucide-react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createModelConnectionMutationAtom,
|
||||
deleteModelConnectionMutationAtom,
|
||||
previewConnectionModelsMutationAtom,
|
||||
testPreviewModelMutationAtom,
|
||||
updateModelRolesMutationAtom,
|
||||
} from "@/atoms/model-connections/model-connections-mutation.atoms";
|
||||
import {
|
||||
|
|
@ -53,24 +55,6 @@ import {
|
|||
providerIcon,
|
||||
} from "./model-connections/provider-metadata";
|
||||
|
||||
function StatusBadge({ connection }: { connection: ConnectionRead }) {
|
||||
if (connection.last_status === "OK") {
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1 text-green-600">
|
||||
<CheckCircle2 className="h-3 w-3" /> Healthy
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (connection.last_status) {
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1 text-destructive">
|
||||
<XCircle className="h-3 w-3" /> {connection.last_status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <Badge variant="secondary">Not tested</Badge>;
|
||||
}
|
||||
|
||||
function flattenModels(connections: ConnectionRead[]) {
|
||||
return connections.flatMap((connection) =>
|
||||
connection.models.map((model) => ({
|
||||
|
|
@ -110,7 +94,6 @@ function ConnectionCard({ connection }: { connection: ConnectionRead }) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<StatusBadge connection={connection} />
|
||||
<ConnectionSettingsDialog connection={connection} providerLabel={providerLabel} />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
|
|
@ -156,6 +139,7 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
|
|||
const [{ data: roles }] = useAtom(modelRolesAtom);
|
||||
const createConnection = useAtomValue(createModelConnectionMutationAtom);
|
||||
const previewModels = useAtomValue(previewConnectionModelsMutationAtom);
|
||||
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
|
||||
const updateRoles = useAtomValue(updateModelRolesMutationAtom);
|
||||
|
||||
const [isAddProviderOpen, setIsAddProviderOpen] = useState(false);
|
||||
|
|
@ -220,9 +204,7 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
|
|||
});
|
||||
}
|
||||
|
||||
// Each provider connect form builds its own credential payload; the backend
|
||||
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
|
||||
function handleCreate(draft: ConnectionDraft) {
|
||||
function connectionModelsForDraft(draft: ConnectionDraft) {
|
||||
const models = [...connectModels];
|
||||
if (draft.seedModelId && !models.some((model) => model.model_id === draft.seedModelId)) {
|
||||
models.push({
|
||||
|
|
@ -233,22 +215,46 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
|
|||
metadata: {},
|
||||
});
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
createConnection.mutate(
|
||||
function representativeTestModel(models: ModelSelection[]) {
|
||||
const enabledModels = models.filter((model) => model.enabled);
|
||||
return enabledModels.find((model) => capability(model, "chat")) ?? enabledModels[0];
|
||||
}
|
||||
|
||||
// Each provider connect form builds its own credential payload; the backend
|
||||
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
|
||||
function handleCreate(draft: ConnectionDraft) {
|
||||
const models = connectionModelsForDraft(draft);
|
||||
const testModel = representativeTestModel(models);
|
||||
if (!testModel) {
|
||||
toast.error("Select at least one model before connecting");
|
||||
return;
|
||||
}
|
||||
|
||||
const request = {
|
||||
provider,
|
||||
base_url: draft.base_url,
|
||||
api_key: draft.api_key,
|
||||
scope: "SEARCH_SPACE" as const,
|
||||
search_space_id: searchSpaceId,
|
||||
extra: draft.extra,
|
||||
enabled: true,
|
||||
models,
|
||||
};
|
||||
|
||||
testPreviewModel.mutate(
|
||||
{ ...request, model_id: testModel.model_id },
|
||||
{
|
||||
provider,
|
||||
base_url: draft.base_url,
|
||||
api_key: draft.api_key,
|
||||
scope: "SEARCH_SPACE",
|
||||
search_space_id: searchSpaceId,
|
||||
extra: draft.extra,
|
||||
enabled: true,
|
||||
models,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsAddProviderOpen(false);
|
||||
resetConnectState();
|
||||
onSuccess: (result) => {
|
||||
if (!result.ok) return;
|
||||
createConnection.mutate(request, {
|
||||
onSuccess: () => {
|
||||
setIsAddProviderOpen(false);
|
||||
resetConnectState();
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -380,7 +386,7 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
|
|||
onOpenChange={handleConnectOpenChange}
|
||||
provider={provider}
|
||||
selectedProvider={selectedProvider}
|
||||
isPending={createConnection.isPending}
|
||||
isPending={createConnection.isPending || testPreviewModel.isPending}
|
||||
onSubmit={handleCreate}
|
||||
previewModels={connectModels}
|
||||
isPreviewingModels={previewModels.isPending}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import {
|
|||
addManualModelMutationAtom,
|
||||
bulkUpdateModelsMutationAtom,
|
||||
discoverConnectionModelsMutationAtom,
|
||||
testPreviewModelMutationAtom,
|
||||
updateModelConnectionMutationAtom,
|
||||
updateModelMutationAtom,
|
||||
verifyModelConnectionMutationAtom,
|
||||
} from "@/atoms/model-connections/model-connections-mutation.atoms";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -26,7 +26,7 @@ import type {
|
|||
ConnectionRead,
|
||||
ConnectionUpdateRequest,
|
||||
} from "@/contracts/types/model-connections.types";
|
||||
import type { SelectableModel } from "./model-utils";
|
||||
import { capability, type SelectableModel } from "./model-utils";
|
||||
import { ModelsSelectionPanel } from "./models-selection-panel";
|
||||
import { providerIcon } from "./provider-metadata";
|
||||
|
||||
|
|
@ -39,8 +39,8 @@ export function ConnectionSettingsDialog({
|
|||
connection,
|
||||
providerLabel,
|
||||
}: ConnectionSettingsDialogProps) {
|
||||
const verifyConnection = useAtomValue(verifyModelConnectionMutationAtom);
|
||||
const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom);
|
||||
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
|
||||
const updateConnection = useAtomValue(updateModelConnectionMutationAtom);
|
||||
const addManualModel = useAtomValue(addManualModelMutationAtom);
|
||||
const updateModel = useAtomValue(updateModelMutationAtom);
|
||||
|
|
@ -81,11 +81,45 @@ export function ConnectionSettingsDialog({
|
|||
if (apiKeyDraft.trim() !== (connection.api_key ?? "")) {
|
||||
data.api_key = apiKeyDraft.trim() || null;
|
||||
}
|
||||
const apiKeyForTest = Object.hasOwn(data, "api_key")
|
||||
? (data.api_key ?? null)
|
||||
: (connection.api_key ?? null);
|
||||
|
||||
updateConnection.mutate(
|
||||
{ id: connection.id, data },
|
||||
const enabledModels = connection.models.filter((model) => model.enabled);
|
||||
const testModel =
|
||||
enabledModels.find((model) => capability(model, "chat")) ?? enabledModels[0];
|
||||
if (!testModel) {
|
||||
updateConnection.mutate(
|
||||
{ id: connection.id, data },
|
||||
{
|
||||
onSuccess: () => setApiKeyDraft(""),
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
testPreviewModel.mutate(
|
||||
{
|
||||
onSuccess: () => setApiKeyDraft(""),
|
||||
provider: connection.provider,
|
||||
base_url: data.base_url,
|
||||
api_key: apiKeyForTest,
|
||||
scope: "SEARCH_SPACE",
|
||||
search_space_id: connection.search_space_id,
|
||||
extra: connection.extra ?? {},
|
||||
enabled: connection.enabled,
|
||||
models: [],
|
||||
model_id: testModel.model_id,
|
||||
},
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
if (!result.ok) return;
|
||||
updateConnection.mutate(
|
||||
{ id: connection.id, data },
|
||||
{
|
||||
onSuccess: () => setApiKeyDraft(""),
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -219,26 +253,15 @@ export function ConnectionSettingsDialog({
|
|||
onBulkToggle={handleBulkToggle}
|
||||
/>
|
||||
|
||||
{connection.last_status && connection.last_status !== "OK" ? (
|
||||
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-sm text-amber-600 dark:text-amber-500">
|
||||
{connection.last_error || "Could not list models."} Chat may still work; add model
|
||||
IDs manually if discovery is unavailable.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="shrink-0 border-t bg-popover px-6 py-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => verifyConnection.mutate(connection.id)}
|
||||
disabled={verifyConnection.isPending}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveConnectionSettings}
|
||||
disabled={updateConnection.isPending || !hasConnectionChanges}
|
||||
disabled={
|
||||
updateConnection.isPending || testPreviewModel.isPending || !hasConnectionChanges
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ export function ProviderConnectDialog({
|
|||
})();
|
||||
|
||||
const canRefreshModels = !isAzure && !isVertex && (!isBedrock || canSubmit);
|
||||
const hasEnabledModel = previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId);
|
||||
const canConnect = canSubmit && hasEnabledModel;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
|
@ -134,7 +136,7 @@ export function ProviderConnectDialog({
|
|||
<ConnectFormFooter
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onSubmit={() => onSubmit(currentDraft)}
|
||||
canSubmit={canSubmit}
|
||||
canSubmit={canConnect}
|
||||
isPending={isPending}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue