mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-30 10:39:30 +01:00
Add the vector policy source embedding controls
This commit is contained in:
@@ -13,6 +13,7 @@ export interface CollapsibleSectionProps {
|
||||
onDelete?: () => void;
|
||||
disabled?: boolean;
|
||||
disableDelete?: boolean;
|
||||
dataTest?: string;
|
||||
}
|
||||
|
||||
export interface CollapsibleSectionState {
|
||||
@@ -57,6 +58,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-expanded={this.state.isExpanded}
|
||||
data-test={this.props.dataTest}
|
||||
>
|
||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>{this.props.title}</Label>
|
||||
|
||||
@@ -2,8 +2,14 @@ import "@testing-library/jest-dom";
|
||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import React from "react";
|
||||
import * as CapabilityUtils from "Utils/CapabilityUtils";
|
||||
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
|
||||
|
||||
// The embedding-source UI is gated behind the integrated-embedding capability.
|
||||
// Default the mock to `true` for the existing suites so they exercise the full UI;
|
||||
// the dedicated suite at the bottom of this file flips it to `false` to verify gating.
|
||||
jest.spyOn(CapabilityUtils, "isIntegratedEmbeddingEnabled").mockReturnValue(true);
|
||||
|
||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||
];
|
||||
@@ -81,3 +87,197 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("VectorEmbeddingPoliciesComponent - embedding source", () => {
|
||||
const newEmbedding: VectorEmbedding[] = [
|
||||
{ path: "/vector2", dataType: "float32", distanceFunction: "cosine", dimensions: 1536 },
|
||||
];
|
||||
let onChange: jest.Mock;
|
||||
let view: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
onChange = jest.fn();
|
||||
view = render(
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddingsBaseline={[]}
|
||||
vectorEmbeddings={newEmbedding}
|
||||
vectorIndexes={[]}
|
||||
onVectorEmbeddingChange={onChange}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const expandSection = () => {
|
||||
const header = screen.getByRole("button", { name: /Embedding source/ });
|
||||
fireEvent.click(header);
|
||||
};
|
||||
|
||||
test("renders the embedding source accordion collapsed by default with no errors", () => {
|
||||
expect(screen.getByText("Embedding source (Preview)")).toBeInTheDocument();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).toBeNull();
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
expect(lastCall[2]).toBe(true);
|
||||
expect(lastCall[0][0].embeddingSource).toBeUndefined();
|
||||
});
|
||||
|
||||
test("expanding the accordion reveals all source fields without errors when empty", () => {
|
||||
expandSection();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).not.toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1")).not.toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-modelName-1")).not.toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1")).not.toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-authType-1")).not.toBeNull();
|
||||
expect(screen.queryByText("At least one source path is required")).toBeNull();
|
||||
expect(screen.queryByText("Deployment name is required")).toBeNull();
|
||||
});
|
||||
|
||||
test("typing in one field surfaces required errors for the others", async () => {
|
||||
expandSection();
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
|
||||
target: { value: "my-deployment" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("At least one source path is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Model name is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Endpoint is required")).toBeInTheDocument();
|
||||
});
|
||||
const last = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
expect(last[2]).toBe(false);
|
||||
});
|
||||
|
||||
test("invalid endpoint shows the https:// error", async () => {
|
||||
expandSection();
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||
target: { value: "not-a-url" },
|
||||
});
|
||||
await waitFor(() => expect(screen.getByText("Endpoint must be a valid https:// URL")).toBeInTheDocument());
|
||||
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||
target: { value: "http://insecure.example.com" },
|
||||
});
|
||||
await waitFor(() => expect(screen.getByText("Endpoint must be a valid https:// URL")).toBeInTheDocument());
|
||||
});
|
||||
|
||||
test("valid input propagates an embeddingSource with parsed sourcePaths", async () => {
|
||||
expandSection();
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
||||
target: { value: "/description, title" },
|
||||
});
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
|
||||
target: { value: "my-deployment" },
|
||||
});
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-modelName-1"), {
|
||||
target: { value: "text-embedding-3-small" },
|
||||
});
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||
target: { value: "https://my-foundry.openai.azure.com" },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
const [embeddings, , valid] = lastCall;
|
||||
expect(valid).toBe(true);
|
||||
expect(embeddings[0].embeddingSource).toEqual({
|
||||
sourcePaths: ["/description", "/title"],
|
||||
deploymentName: "my-deployment",
|
||||
modelName: "text-embedding-3-small",
|
||||
endpoint: "https://my-foundry.openai.azure.com",
|
||||
authType: "Entra",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("clearing all fields drops embeddingSource from the emitted embedding", async () => {
|
||||
expandSection();
|
||||
const sourcePaths = view.container.querySelector(
|
||||
"#vector-policy-embeddingSource-sourcePaths-1",
|
||||
) as HTMLInputElement;
|
||||
const deploymentName = view.container.querySelector(
|
||||
"#vector-policy-embeddingSource-deploymentName-1",
|
||||
) as HTMLInputElement;
|
||||
const modelName = view.container.querySelector("#vector-policy-embeddingSource-modelName-1") as HTMLInputElement;
|
||||
const endpoint = view.container.querySelector("#vector-policy-embeddingSource-endpoint-1") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(sourcePaths, { target: { value: "/description" } });
|
||||
fireEvent.change(deploymentName, { target: { value: "d" } });
|
||||
fireEvent.change(modelName, { target: { value: "m" } });
|
||||
fireEvent.change(endpoint, { target: { value: "https://x.example.com" } });
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
expect(lastCall[2]).toBe(true);
|
||||
expect(lastCall[0][0].embeddingSource).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.change(sourcePaths, { target: { value: "" } });
|
||||
fireEvent.change(deploymentName, { target: { value: "" } });
|
||||
fireEvent.change(modelName, { target: { value: "" } });
|
||||
fireEvent.change(endpoint, { target: { value: "" } });
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
const [embeddings, , valid] = lastCall;
|
||||
expect(valid).toBe(true);
|
||||
expect(embeddings[0].embeddingSource).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("does not loop onVectorEmbeddingChange once all fields become valid (regression)", async () => {
|
||||
// Regression for an infinite render loop where the child rebuilt a fresh
|
||||
// VectorEmbeddingSource literal on every render, defeating the parent's
|
||||
// reference-equality dedupe and re-triggering child useEffect via the
|
||||
// unstable onChange prop. Before the fix, this scenario hung the test
|
||||
// runner (Jest would time out after several minutes).
|
||||
expandSection();
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
|
||||
target: { value: "/title, /description" },
|
||||
});
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
|
||||
target: { value: "text-embedding-ada-002" },
|
||||
});
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-modelName-1"), {
|
||||
target: { value: "text-embedding-ada-002" },
|
||||
});
|
||||
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
|
||||
target: { value: "https://my-foundry.openai.azure.com" },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
||||
expect(lastCall[2]).toBe(true);
|
||||
expect(lastCall[0][0].embeddingSource).toBeDefined();
|
||||
});
|
||||
|
||||
const stable = onChange.mock.calls.length;
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(onChange.mock.calls.length).toBe(stable);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VectorEmbeddingPoliciesComponent - embedding source gating", () => {
|
||||
const newEmbedding: VectorEmbedding[] = [
|
||||
{ path: "/vector3", dataType: "float32", distanceFunction: "cosine", dimensions: 1536 },
|
||||
];
|
||||
|
||||
test("hides the embedding source accordion when the integrated-embedding capability is missing", () => {
|
||||
const isEnabledSpy = jest.spyOn(CapabilityUtils, "isIntegratedEmbeddingEnabled").mockReturnValue(false);
|
||||
try {
|
||||
const view = render(
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddingsBaseline={[]}
|
||||
vectorEmbeddings={newEmbedding}
|
||||
vectorIndexes={[]}
|
||||
onVectorEmbeddingChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("Embedding source (Preview)")).toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1")).toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-modelName-1")).toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1")).toBeNull();
|
||||
expect(view.container.querySelector("#vector-policy-embeddingSource-authType-1")).toBeNull();
|
||||
} finally {
|
||||
isEnabledSpy.mockReturnValue(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IStyleFunctionOrObject,
|
||||
ITextFieldStyleProps,
|
||||
ITextFieldStyles,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { DefaultButton, Dropdown, IDropdownOption, Label, Stack, TextField } from "@fluentui/react";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { VectorEmbedding, VectorEmbeddingSource, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { VectorEmbeddingSourceComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingSourceComponent";
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
getDistanceFunctionOptions,
|
||||
@@ -19,8 +10,15 @@ import {
|
||||
getQuantizerTypeOptions,
|
||||
supportsQuantization,
|
||||
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
|
||||
import { dropdownStyles, labelStyles, textFieldStyles } from "Explorer/Controls/VectorSearch/vectorSearchStyles";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { isIntegratedEmbeddingEnabled } from "Utils/CapabilityUtils";
|
||||
|
||||
const generatePolicyId = (): string =>
|
||||
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `vep-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
export interface IVectorEmbeddingPoliciesComponentProps {
|
||||
vectorEmbeddingsBaseline: VectorEmbedding[];
|
||||
@@ -37,6 +35,7 @@ export interface IVectorEmbeddingPoliciesComponentProps {
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicyData {
|
||||
id: string;
|
||||
path: string;
|
||||
dataType: VectorEmbedding["dataType"];
|
||||
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||
@@ -50,44 +49,12 @@ export interface VectorEmbeddingPolicyData {
|
||||
quantizationByteSize?: number;
|
||||
quantizationByteSizeError?: string;
|
||||
quantizerType?: VectorIndex["quantizerType"];
|
||||
embeddingSource?: VectorEmbeddingSource;
|
||||
embeddingSourceValid: boolean;
|
||||
}
|
||||
|
||||
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||
|
||||
const labelStyles = {
|
||||
root: {
|
||||
fontSize: 12,
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
};
|
||||
|
||||
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||
fieldGroup: {
|
||||
height: 27,
|
||||
},
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
backgroundColor: "var(--colorNeutralBackground1)",
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownStyles = {
|
||||
title: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
fontSize: 12,
|
||||
},
|
||||
dropdown: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
},
|
||||
dropdownItem: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
|
||||
vectorEmbeddingsBaseline,
|
||||
vectorEmbeddings,
|
||||
@@ -163,6 +130,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
const supportsQuantizer = supportsQuantization(matchingType);
|
||||
mergedData.push({
|
||||
...embedding,
|
||||
id: generatePolicyId(),
|
||||
indexType: matchingType || "none",
|
||||
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
|
||||
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
|
||||
@@ -170,6 +138,8 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||
embeddingSource: embedding.embeddingSource,
|
||||
embeddingSourceValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,12 +163,18 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
}, [discardChanges]);
|
||||
|
||||
const propagateData = () => {
|
||||
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||
path: policy.path,
|
||||
dataType: policy.dataType,
|
||||
dimensions: policy.dimensions,
|
||||
distanceFunction: policy.distanceFunction,
|
||||
}));
|
||||
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => {
|
||||
const base: VectorEmbedding = {
|
||||
path: policy.path,
|
||||
dataType: policy.dataType,
|
||||
dimensions: policy.dimensions,
|
||||
distanceFunction: policy.distanceFunction,
|
||||
};
|
||||
if (policy.embeddingSource) {
|
||||
base.embeddingSource = policy.embeddingSource;
|
||||
}
|
||||
return base;
|
||||
});
|
||||
const vectorIndexes: VectorIndex[] = vectorEmbeddingPolicyData
|
||||
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||
.map(
|
||||
@@ -215,7 +191,8 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
}) as VectorIndex,
|
||||
);
|
||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||
(policy: VectorEmbeddingPolicyData) =>
|
||||
policy.pathError === "" && policy.dimensionsError === "" && policy.embeddingSourceValid,
|
||||
);
|
||||
|
||||
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
|
||||
@@ -306,10 +283,29 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onEmbeddingSourceChange = useCallback(
|
||||
(index: number, embeddingSource: VectorEmbeddingSource | undefined, isValid: boolean): void => {
|
||||
setVectorEmbeddingPolicyData((prev) => {
|
||||
const current = prev[index];
|
||||
if (!current) {
|
||||
return prev;
|
||||
}
|
||||
if (current.embeddingSource === embeddingSource && current.embeddingSourceValid === isValid) {
|
||||
return prev;
|
||||
}
|
||||
const next = [...prev];
|
||||
next[index] = { ...current, embeddingSource, embeddingSourceValid: isValid };
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onAdd = () => {
|
||||
setVectorEmbeddingPolicyData([
|
||||
...vectorEmbeddingPolicyData,
|
||||
{
|
||||
id: generatePolicyId(),
|
||||
path: "",
|
||||
dataType: "float32",
|
||||
distanceFunction: "euclidean",
|
||||
@@ -317,6 +313,8 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
indexType: "none",
|
||||
pathError: onVectorEmbeddingPathError(""),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||
embeddingSource: undefined,
|
||||
embeddingSourceValid: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -340,12 +338,13 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||
<CollapsibleSectionComponent
|
||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||
key={index}
|
||||
key={vectorEmbeddingPolicy.id}
|
||||
isExpandedByDefault={true}
|
||||
title={t(Keys.controls.vectorEmbeddingPolicies.vectorEmbeddingTitle, { index: index + 1 })}
|
||||
showDelete={true}
|
||||
onDelete={() => onDelete(index)}
|
||||
disableDelete={false}
|
||||
dataTest={`VectorEmbedding/Section/${index + 1}`}
|
||||
>
|
||||
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||
<Stack
|
||||
@@ -365,6 +364,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
<TextField
|
||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||
id={`vector-policy-path-${index + 1}`}
|
||||
data-test={`VectorEmbedding/Path/${index + 1}`}
|
||||
required={true}
|
||||
placeholder="/vector1"
|
||||
styles={textFieldStyles}
|
||||
@@ -410,6 +410,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
<TextField
|
||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||
id={`vector-policy-dimension-${index + 1}`}
|
||||
data-test={`VectorEmbedding/Dimensions/${index + 1}`}
|
||||
required={true}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||
@@ -525,11 +526,25 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
{isIntegratedEmbeddingEnabled() && (
|
||||
<VectorEmbeddingSourceComponent
|
||||
index={index}
|
||||
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
|
||||
initialEmbeddingSource={vectorEmbeddingPolicy.embeddingSource}
|
||||
discardChanges={discardChanges}
|
||||
onChange={(source, isValid) => onEmbeddingSourceChange(index, source, isValid)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
))}
|
||||
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||
<DefaultButton
|
||||
id={`add-vector-policy`}
|
||||
data-test="VectorEmbedding/AddButton"
|
||||
styles={{ root: { maxWidth: 170, fontSize: 12 } }}
|
||||
onClick={onAdd}
|
||||
>
|
||||
{t(Keys.controls.vectorEmbeddingPolicies.addVectorEmbedding)}
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { IDropdownOption } from "@fluentui/react";
|
||||
import { VectorIndex } from "Contracts/DataModels";
|
||||
import { VectorEmbeddingSource, VectorIndex } from "Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
|
||||
const dataTypes = ["float32", "uint8", "int8", "float16"];
|
||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||
const authTypes: VectorEmbeddingSource["authType"][] = ["Entra"];
|
||||
|
||||
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
|
||||
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
||||
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||
export const getAuthTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(authTypes);
|
||||
export const getQuantizerTypeOptions = (): IDropdownOption[] => [
|
||||
{ key: "product", text: "Product" },
|
||||
{ key: "spherical", text: `Spherical (${t(Keys.common.preview)})` },
|
||||
@@ -17,6 +19,28 @@ export const getQuantizerTypeOptions = (): IDropdownOption[] => [
|
||||
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
|
||||
indexType === "quantizedFlat" || indexType === "diskANN";
|
||||
|
||||
// Parses a comma-separated path list, trims whitespace, drops blanks, and
|
||||
// prefixes a leading "/" on each entry if it isn't there already.
|
||||
export const parseSourcePaths = (raw: string): string[] => {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
return raw
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0)
|
||||
.map((p) => (p.startsWith("/") ? p : `/${p}`));
|
||||
};
|
||||
|
||||
export const isValidHttpsUrl = (value: string): boolean => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
|
||||
return literals.map((value) => ({
|
||||
key: value,
|
||||
|
||||
@@ -838,6 +838,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||
dataTest="ContainerVectorPolicy/Section"
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
|
||||
Reference in New Issue
Block a user