Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Badge } from "@/components/ui/badge";
import {
CommandGroup,
CommandItem,
Expand Down Expand Up @@ -52,6 +53,15 @@ const ModelList = ({
className="h-4 w-4 shrink-0 text-primary ml-2"
/>
<div className="truncate text-[13px]">{data.name}</div>
{data.metadata?.deprecated ? (
<Badge
variant="secondaryStatic"
size="tag"
data-testid={`${data.name}-deprecated-badge`}
>
Deprecated
</Badge>
) : null}
<div className="pl-2 ml-auto">
<ForwardedIconComponent
name="Check"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function KnowledgeBaseUploadModal({
separator={form.separator}
onSeparatorChange={form.setSeparator}
showAdvanced={form.showAdvanced}
toggleAdvanced={form.toggleAdvanced}
hasFiles={form.files.length > 0}
onFileSelect={form.handleFileSelect}
onFolderSelect={form.handleFolderSelect}
validationErrors={form.validationErrors}
Expand Down Expand Up @@ -91,14 +91,11 @@ export default function KnowledgeBaseUploadModal({
};

const errorCount = Object.keys(form.validationErrors).length;
const modalBase =
!hideAdvanced && form.showAdvanced
? MODAL_HEIGHT_WITH_ADVANCED
: MODAL_HEIGHT_DEFAULT;
const modalBase = hideAdvanced
? MODAL_HEIGHT_DEFAULT
: MODAL_HEIGHT_WITH_ADVANCED;
const modalHeight = `${modalBase + errorCount * VALIDATION_ERROR_LINE_HEIGHT}`;

const showHelpButton = !hideAdvanced && form.currentStep === 1;

return (
<StepperModal
open={open}
Expand Down Expand Up @@ -129,7 +126,7 @@ export default function KnowledgeBaseUploadModal({
footer={
<StepperModalFooter
currentStep={form.currentStep}
totalSteps={!hideAdvanced && form.showAdvanced ? 2 : 1}
totalSteps={hideAdvanced ? 1 : 2}
onBack={form.handleBack}
onNext={form.handleNext}
onSubmit={form.handleSubmit}
Expand All @@ -141,14 +138,6 @@ export default function KnowledgeBaseUploadModal({
isSubmitting={form.isSubmitting}
submitTestId="kb-create-button"
submitLabel={form.isAddSourcesMode ? "Add Sources" : "Create"}
helpLabel={
showHelpButton
? form.showAdvanced
? "Hide Configuration"
: "Configure Sources"
: undefined
}
onHelp={showHelpButton ? form.toggleAdvanced : undefined}
/>
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,53 @@ describe("KnowledgeBaseUploadModal", () => {
expect(screen.getByText("Embedding Model")).toBeInTheDocument();
});

it("renders Configure Sources toggle button in footer", () => {
it("renders Ingest Content section open by default", () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
expect(screen.getByText(/Ingest Content/i)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Configure Sources/i }),
screen.getByRole("button", { name: /Add Files/i }),
).toBeInTheDocument();
});

it("does not render the Hide Configuration footer toggle", () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
expect(
screen.queryByRole("button", { name: /Hide Configuration/i }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /^Configure Sources$/i }),
).not.toBeInTheDocument();
});

it("disables chunking inputs until at least one source is added", async () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
expect(screen.getByTestId("kb-chunk-size-input")).toBeDisabled();
expect(screen.getByTestId("kb-chunk-overlap-input")).toBeDisabled();
expect(screen.getByTestId("kb-separator-input")).toBeDisabled();

const fileInput = document.getElementById(
"file-input",
) as HTMLInputElement;
const event = {
target: {
files: [new File(["x"], "doc.txt", { type: "text/plain" })],
},
} as unknown as React.ChangeEvent<HTMLInputElement>;
fireEvent.change(fileInput, event);

await waitFor(() =>
expect(screen.getByTestId("kb-chunk-size-input")).not.toBeDisabled(),
);
expect(screen.getByTestId("kb-chunk-overlap-input")).not.toBeDisabled();
expect(screen.getByTestId("kb-separator-input")).not.toBeDisabled();
});

it('shows "Add Sources" title in add-sources mode', async () => {
render(
<KnowledgeBaseUploadModal
Expand Down Expand Up @@ -272,14 +310,17 @@ describe("KnowledgeBaseUploadModal", () => {
// ── Form Validation ────────────────────────────────────────────────────────

describe("Form Validation", () => {
it("submit button is disabled when form is empty", () => {
it("step 1 shows Next Step button rather than submit", () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
expect(screen.getByTestId("kb-create-button")).toBeDisabled();
expect(
screen.getByRole("button", { name: /Next Step/i }),
).toBeInTheDocument();
expect(screen.queryByTestId("kb-create-button")).not.toBeInTheDocument();
});

it("submit button is disabled when only source name is filled", async () => {
it("Next Step does not advance when only source name is filled", async () => {
const user = userEvent.setup();
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
Expand All @@ -288,16 +329,23 @@ describe("KnowledgeBaseUploadModal", () => {
screen.getByTestId("kb-source-name-input"),
"MyKnowledgeBase",
);
expect(screen.getByTestId("kb-create-button")).toBeDisabled();
await user.click(screen.getByRole("button", { name: /Next Step/i }));
expect(
screen.getByText("Embedding model is required"),
).toBeInTheDocument();
expect(screen.queryByTestId("kb-create-button")).not.toBeInTheDocument();
});

it("submit button is enabled when name and embedding model are both provided", async () => {
it("submit button enabled on step 2 when name and embedding model are provided", async () => {
const user = userEvent.setup();
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
await fillRequiredFields(user);
expect(screen.getByTestId("kb-create-button")).not.toBeDisabled();
await user.click(screen.getByRole("button", { name: /Next Step/i }));
await waitFor(() =>
expect(screen.getByTestId("kb-create-button")).not.toBeDisabled(),
);
});

it("shows inline error when name is shorter than 3 characters", async () => {
Expand All @@ -310,7 +358,7 @@ describe("KnowledgeBaseUploadModal", () => {
screen.getByTestId("embedding-model-select"),
"text-embedding-3-small",
);
await user.click(screen.getByTestId("kb-create-button"));
await user.click(screen.getByRole("button", { name: /Next Step/i }));
await waitFor(() =>
expect(
screen.getByText("Name must be between 3 and 512 characters"),
Expand All @@ -328,7 +376,7 @@ describe("KnowledgeBaseUploadModal", () => {
screen.getByTestId("embedding-model-select"),
"text-embedding-3-small",
);
await user.click(screen.getByTestId("kb-create-button"));
await user.click(screen.getByRole("button", { name: /Next Step/i }));
await waitFor(() =>
expect(screen.getByText(/Name must only contain/)).toBeInTheDocument(),
);
Expand All @@ -345,7 +393,7 @@ describe("KnowledgeBaseUploadModal", () => {
{ wrapper: createWrapper() },
);
await fillRequiredFields(user);
await user.click(screen.getByTestId("kb-create-button"));
await user.click(screen.getByRole("button", { name: /Next Step/i }));
await waitFor(() =>
expect(
screen.getByText("A knowledge base with this name already exists"),
Expand All @@ -357,12 +405,22 @@ describe("KnowledgeBaseUploadModal", () => {
// ── Form Submission ────────────────────────────────────────────────────────

describe("Form Submission", () => {
const advanceToReview = async (
user: ReturnType<typeof userEvent.setup>,
) => {
await fillRequiredFields(user);
await user.click(screen.getByRole("button", { name: /Next Step/i }));
await waitFor(() =>
expect(screen.getByTestId("kb-create-button")).toBeInTheDocument(),
);
};

it("calls mutateAsync with correct payload on valid submission", async () => {
const user = userEvent.setup();
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
await fillRequiredFields(user);
await advanceToReview(user);
await user.click(screen.getByTestId("kb-create-button"));
await waitFor(() =>
expect(mockMutateAsync).toHaveBeenCalledWith({
Expand All @@ -381,7 +439,7 @@ describe("KnowledgeBaseUploadModal", () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
await fillRequiredFields(user);
await advanceToReview(user);
await user.click(screen.getByTestId("kb-create-button"));
await waitFor(() =>
expect(mockSetSuccessData).toHaveBeenCalledWith({
Expand All @@ -396,7 +454,7 @@ describe("KnowledgeBaseUploadModal", () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={mockSetOpen} />, {
wrapper: createWrapper(),
});
await fillRequiredFields(user);
await advanceToReview(user);
await user.click(screen.getByTestId("kb-create-button"));
await waitFor(() => expect(mockSetOpen).toHaveBeenCalledWith(false));
});
Expand All @@ -412,7 +470,7 @@ describe("KnowledgeBaseUploadModal", () => {
/>,
{ wrapper: createWrapper() },
);
await fillRequiredFields(user);
await advanceToReview(user);
await user.click(screen.getByTestId("kb-create-button"));
await waitFor(() => expect(mockOnSubmit).toHaveBeenCalled());
expect(mockOnSubmit.mock.calls[0][0]).toMatchObject({
Expand All @@ -429,7 +487,7 @@ describe("KnowledgeBaseUploadModal", () => {
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
await fillRequiredFields(user);
await advanceToReview(user);
await user.click(screen.getByTestId("kb-create-button"));
await waitFor(() =>
expect(mockSetErrorData).toHaveBeenCalledWith({
Expand Down Expand Up @@ -463,14 +521,11 @@ describe("KnowledgeBaseUploadModal", () => {
expect(input).toHaveValue("");
});

it("opens file-upload dropdown when Add Sources button is clicked", async () => {
it("opens file-upload dropdown when Add Files button is clicked", async () => {
const user = userEvent.setup();
render(<KnowledgeBaseUploadModal open={true} setOpen={jest.fn()} />, {
wrapper: createWrapper(),
});
await user.click(
screen.getByRole("button", { name: /Configure Sources/i }),
);
await user.click(screen.getByTestId("kb-browse-btn"));
expect(screen.getByText("Upload Files")).toBeInTheDocument();
expect(screen.getByText("Upload Folder")).toBeInTheDocument();
Expand Down Expand Up @@ -623,9 +678,6 @@ describe("KnowledgeBaseUploadModal", () => {
name = "TestKnowledgeBase",
) => {
await fillRequiredFields(user, name);
await user.click(
screen.getByRole("button", { name: /Configure Sources/i }),
);
await user.click(screen.getByRole("button", { name: /Next Step/i }));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface StepConfigurationProps {
separator: string;
onSeparatorChange: (value: string) => void;
showAdvanced: boolean;
toggleAdvanced: () => void;
hasFiles: boolean;
onFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFolderSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
validationErrors?: Record<string, string>;
Expand All @@ -64,7 +64,7 @@ export function StepConfiguration({
separator,
onSeparatorChange,
showAdvanced,
toggleAdvanced,
hasFiles,
onFileSelect,
onFolderSelect,
validationErrors = {},
Expand Down Expand Up @@ -166,7 +166,7 @@ export function StepConfiguration({
} as React.HTMLAttributes<HTMLInputElement>)}
/>

{/* Configure Sources - Animated */}
{/* Ingest Content - Animated */}
<div
className={cn(
"grid transition-all duration-300 ease-in-out",
Expand All @@ -184,7 +184,7 @@ export function StepConfiguration({
className="h-4 w-4 text-muted-foreground"
/>
<span className="text-sm font-medium">
Configure Sources
Ingest Content
<span className="text-xs text-muted-foreground ml-1">
(1 GB max upload)
</span>
Expand All @@ -202,7 +202,7 @@ export function StepConfiguration({
variant="outline"
data-testid="kb-browse-btn"
className={cn(
"w-full justify-between focus-visible:ring-1 focus-visible:ring-offset-0 focus-visible:ring-offset-background ",
"w-full justify-between focus-visible:ring-1 focus-visible:ring-input focus-visible:ring-offset-0 focus-visible:ring-offset-background",
validationErrors.files && "border-destructive",
)}
>
Expand All @@ -211,7 +211,7 @@ export function StepConfiguration({
name="Upload"
className="h-4 w-4"
/>
Add Sources
Add Files
</span>
<ForwardedIconComponent
name="ChevronDown"
Expand Down Expand Up @@ -291,7 +291,13 @@ export function StepConfiguration({
>
<div className="overflow-hidden">
<Separator className="my-4" />
<div className="flex flex-col gap-4">
<div
className={cn(
"flex flex-col gap-4 transition-opacity",
!hasFiles && "opacity-50",
)}
aria-disabled={!hasFiles}
>
<div className="flex items-center gap-2">
<ForwardedIconComponent
name="Settings2"
Expand Down Expand Up @@ -334,6 +340,7 @@ export function StepConfiguration({
}
min={1}
max={10000}
disabled={!hasFiles}
data-testid="kb-chunk-size-input"
/>
</div>
Expand Down Expand Up @@ -370,6 +377,7 @@ export function StepConfiguration({
}
min={0}
max={chunkSize - 1}
disabled={!hasFiles}
data-testid="kb-chunk-overlap-input"
/>
</div>
Expand Down Expand Up @@ -405,6 +413,7 @@ export function StepConfiguration({
placeholder="\n"
value={separator}
onChange={(e) => onSeparatorChange(e.target.value)}
disabled={!hasFiles}
data-testid="kb-separator-input"
/>
</div>
Expand Down
Loading
Loading