Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
Expand Up @@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { cn } from "../../ui/primitives/styles";
import { Tabs, TabsContent } from "../../ui/primitives/tabs";
import { useCrawlUrl, useUploadDocument } from "../hooks";
import type { CrawlRequest, UploadMetadata } from "../types";
import type { CrawlRequest, KnowledgeItemsFilter, UploadMetadata } from "../types";
import { KnowledgeTypeSelector } from "./KnowledgeTypeSelector";
import { LevelSelector } from "./LevelSelector";
import { TagInput } from "./TagInput";
Expand All @@ -21,18 +21,20 @@ interface AddKnowledgeDialogProps {
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
onCrawlStarted?: (progressId: string) => void;
currentFilter?: KnowledgeItemsFilter;
}

export const AddKnowledgeDialog: React.FC<AddKnowledgeDialogProps> = ({
open,
onOpenChange,
onSuccess,
onCrawlStarted,
currentFilter,
}) => {
const [activeTab, setActiveTab] = useState<"crawl" | "upload">("crawl");
const { showToast } = useToast();
const crawlMutation = useCrawlUrl();
const uploadMutation = useUploadDocument();
const crawlMutation = useCrawlUrl(currentFilter);
const uploadMutation = useUploadDocument(currentFilter);

// Generate unique IDs for form elements
const urlId = useId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Knowledge Filter Context
* Provides shared filter state between KnowledgeView components and hooks
*
* This solves the optimistic updates issue where mutations need to know
* the current filter state to update the correct cached query.
*/

import React, { createContext, useContext, useMemo, useState } from "react";
import type { KnowledgeItemsFilter } from "../types";

interface KnowledgeFilterContextType {
// Current filter state
searchQuery: string;
typeFilter: "all" | "technical" | "business";

// Filter state setters
setSearchQuery: (query: string) => void;
setTypeFilter: (type: "all" | "technical" | "business") => void;

// Computed filter object for API queries
currentFilter: KnowledgeItemsFilter;
}

const KnowledgeFilterContext = createContext<KnowledgeFilterContextType | null>(null);

interface KnowledgeFilterProviderProps {
children: React.ReactNode;
}

/**
* Provider component that manages knowledge filter state
*/
export function KnowledgeFilterProvider({ children }: KnowledgeFilterProviderProps) {
const [searchQuery, setSearchQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<"all" | "technical" | "business">("all");

// Build filter object for API - memoize to prevent recreating on every render
const currentFilter = useMemo<KnowledgeItemsFilter>(() => {
const filter: KnowledgeItemsFilter = {
page: 1,
per_page: 100,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Hard-coded pagination defaults can drift from API/query keys

Page/per_page are fixed to 1/100 here. If server/query-key defaults differ elsewhere, caches fragment and optimistic updates won’t land where expected. Extract shared defaults and import them, or read them from the query-key builder used by useKnowledgeSummaries.

-  const currentFilter = useMemo<KnowledgeItemsFilter>(() => {
-    const filter: KnowledgeItemsFilter = {
-      page: 1,
-      per_page: 100,
-    };
+  const DEFAULT_PAGE = 1;
+  const DEFAULT_PER_PAGE = 100;
+  const currentFilter = useMemo<KnowledgeItemsFilter>(() => {
+    const filter: KnowledgeItemsFilter = {
+      page: DEFAULT_PAGE,
+      per_page: DEFAULT_PER_PAGE,
+    };

🏁 Script executed:

#!/bin/bash
# Ensure pagination defaults match across feature
rg -nP '\b(page|per_page)\s*:\s*\d+' archon-ui-main/src/features/knowledge --type=ts -C2

Length of output: 10803


Centralize pagination defaults (page / per_page) across the knowledge feature

Hard-coded defaults appear in many places (most use page: 1, per_page: 100) but at least one test uses per_page: 20 — extract a single shared DEFAULT_PAGE/DEFAULT_PER_PAGE (or read from the query-key builder/knowledgeKeys) and import it everywhere to avoid cache/query-key mismatches and brittle tests.

Relevant locations:

  • archon-ui-main/src/features/knowledge/context/KnowledgeFilterContext.tsx (current 1/100)
  • archon-ui-main/src/features/knowledge/hooks/useKnowledgeQueries.ts (optimistic updates use 1/100)
  • archon-ui-main/src/features/knowledge/hooks/tests/useKnowledgeQueries.test.ts (uses per_page: 20)
🤖 Prompt for AI Agents
In archon-ui-main/src/features/knowledge/context/KnowledgeFilterContext.tsx
around lines 39-43, the pagination defaults (page: 1, per_page: 100) are
hard-coded; extract shared constants (e.g., DEFAULT_PAGE and DEFAULT_PER_PAGE)
into a single module (or reuse the query-key builder/knowledgeKeys export if
available), replace the inline literals here with those constants, update
useKnowledgeQueries.ts to reference the same constants for optimistic updates,
and modify the test useKnowledgeQueries.test.ts to use the shared
DEFAULT_PER_PAGE (or update its expected value to match the centralized
constant) so all places import the same source of truth and avoid mismatched
cache/query keys and brittle tests.


if (searchQuery) {
filter.search = searchQuery;
}

if (typeFilter !== "all") {
filter.knowledge_type = typeFilter;
}

return filter;
}, [searchQuery, typeFilter]);

const value = useMemo<KnowledgeFilterContextType>(
() => ({
searchQuery,
typeFilter,
setSearchQuery,
setTypeFilter,
currentFilter,
}),
[searchQuery, typeFilter, currentFilter]
);

return (
<KnowledgeFilterContext.Provider value={value}>
{children}
</KnowledgeFilterContext.Provider>
);
}

/**
* Hook to access knowledge filter context
* @throws Error if used outside of KnowledgeFilterProvider
*/
export function useKnowledgeFilter() {
const context = useContext(KnowledgeFilterContext);

if (!context) {
throw new Error("useKnowledgeFilter must be used within a KnowledgeFilterProvider");
}

return context;
}
6 changes: 6 additions & 0 deletions archon-ui-main/src/features/knowledge/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Knowledge Context Module
* Exports all context providers and hooks
*/

export { KnowledgeFilterProvider, useKnowledgeFilter } from "./KnowledgeFilterContext";
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { describe, it, expect } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { KnowledgeFilterProvider, useKnowledgeFilter } from '../KnowledgeFilterContext';
import type { KnowledgeFilter } from '../../types';

// Test component that uses the context
function TestComponent() {
const { currentFilter, searchQuery, typeFilter, setSearchQuery, setTypeFilter } = useKnowledgeFilter();

return (
<div>
<div data-testid="current-filter">{JSON.stringify(currentFilter)}</div>
<div data-testid="search-query">{searchQuery}</div>
<div data-testid="type-filter">{typeFilter}</div>
<button
data-testid="update-search"
onClick={() => setSearchQuery('test search')}
>
Update Search
</button>
<button
data-testid="update-type"
onClick={() => setTypeFilter('technical')}
>
Update Type
</button>
</div>
);
}

function TestComponentWithoutProvider() {
const hook = useKnowledgeFilter();
return <div data-testid="hook-result">{JSON.stringify(hook)}</div>;
}

describe('KnowledgeFilterContext', () => {
describe('KnowledgeFilterProvider', () => {
it('should provide default filter state', () => {
render(
<KnowledgeFilterProvider>
<TestComponent />
</KnowledgeFilterProvider>
);

const filterDisplay = screen.getByTestId('current-filter');
const defaultFilter = JSON.parse(filterDisplay.textContent || '{}');

expect(defaultFilter).toEqual({
page: 1,
per_page: 100
});

const searchQuery = screen.getByTestId('search-query');
const typeFilter = screen.getByTestId('type-filter');

expect(searchQuery.textContent).toBe('');
expect(typeFilter.textContent).toBe('all');
});

it('should update search query when setSearchQuery is called', async () => {
const user = userEvent.setup();

render(
<KnowledgeFilterProvider>
<TestComponent />
</KnowledgeFilterProvider>
);

const updateButton = screen.getByTestId('update-search');

await act(async () => {
await user.click(updateButton);
});

const filterDisplay = screen.getByTestId('current-filter');
const updatedFilter = JSON.parse(filterDisplay.textContent || '{}');
const searchQuery = screen.getByTestId('search-query');

expect(searchQuery.textContent).toBe('test search');
expect(updatedFilter).toEqual({
search: 'test search',
page: 1,
per_page: 100
});
});

it('should update type filter when setTypeFilter is called', async () => {
const user = userEvent.setup();

render(
<KnowledgeFilterProvider>
<TestComponent />
</KnowledgeFilterProvider>
);

const updateButton = screen.getByTestId('update-type');

await act(async () => {
await user.click(updateButton);
});

const filterDisplay = screen.getByTestId('current-filter');
const updatedFilter = JSON.parse(filterDisplay.textContent || '{}');
const typeFilter = screen.getByTestId('type-filter');

expect(typeFilter.textContent).toBe('technical');
expect(updatedFilter).toEqual({
knowledge_type: 'technical',
page: 1,
per_page: 100
});
});

it('should merge search and type updates correctly', async () => {
const user = userEvent.setup();

render(
<KnowledgeFilterProvider>
<TestComponent />
</KnowledgeFilterProvider>
);

// Update search first
await act(async () => {
await user.click(screen.getByTestId('update-search'));
});

// Then update type
await act(async () => {
await user.click(screen.getByTestId('update-type'));
});

const filterDisplay = screen.getByTestId('current-filter');
const finalFilter = JSON.parse(filterDisplay.textContent || '{}');

expect(finalFilter).toEqual({
search: 'test search',
knowledge_type: 'technical',
page: 1,
per_page: 100
});
});
});

describe('useKnowledgeFilter', () => {
it('should throw error when used outside provider', () => {
// Suppress console.error for this test
const originalError = console.error;
console.error = () => {};

expect(() => {
render(<TestComponentWithoutProvider />);
}).toThrow('useKnowledgeFilter must be used within a KnowledgeFilterProvider');

console.error = originalError;
});

it('should maintain filter state across re-renders', async () => {
let renderCount = 0;

const TestRerenderComponent = () => {
renderCount++;
const { currentFilter, setSearchQuery } = useKnowledgeFilter();

return (
<div>
<div data-testid="render-count">{renderCount}</div>
<div data-testid="filter-state">{JSON.stringify(currentFilter)}</div>
<button
data-testid="trigger-update"
onClick={() => setSearchQuery('persistent')}
>
Update
</button>
</div>
);
};

const user = userEvent.setup();

render(
<KnowledgeFilterProvider>
<TestRerenderComponent />
</KnowledgeFilterProvider>
);

// Update the filter
await act(async () => {
await user.click(screen.getByTestId('trigger-update'));
});

const filterState = screen.getByTestId('filter-state');
const currentFilter = JSON.parse(filterState.textContent || '{}');

expect(currentFilter.search).toBe('persistent');
expect(renderCount).toBeGreaterThan(1);
});
});
});
Loading