Skip to content

Commit

Permalink
Merge pull request #82 from Renumics/fix/activesources-null-value
Browse files Browse the repository at this point in the history
Fix: ActiveSources + type Sources
  • Loading branch information
dani2112 authored Mar 5, 2025
2 parents 0203b7d + 4f31276 commit 5ac75b4
Show file tree
Hide file tree
Showing 9 changed files with 409 additions and 160 deletions.
4 changes: 2 additions & 2 deletions lexio/lib/components/LexioProvider/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import { Provider, createStore } from 'jotai';
import { configAtom, registeredActionHandlersAtom } from '../../state/rag-state';
import { ActionHandler, RAGConfig } from '../../types';
import { ActionHandler, ProviderConfig } from '../../types';


import { ThemeProvider } from '../../theme/ThemeContext';
Expand All @@ -12,7 +12,7 @@ interface LexioProviderProps {
children: React.ReactNode;
onAction?: ActionHandler['handler'];
theme?: Theme;
config?: RAGConfig;
config?: ProviderConfig;
}


Expand Down
111 changes: 105 additions & 6 deletions lexio/lib/components/SourcesDisplay/SourcesDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '@testing-library/jest-dom';

import { SourcesDisplay } from './SourcesDisplay';
import { LexioProvider } from '../LexioProvider';
import { UUID } from '../../types';

/**
* Utility to create a wrapper that provides:
Expand All @@ -33,8 +34,8 @@ describe('SourcesDisplay (with onAction-based SEARCH_SOURCES)', () => {
return {
// The store will update with these sources after the promise resolves
sources: Promise.resolve([
{ id: 'source1-id', title: 'Mock Source 1', type: 'text' },
{ id: 'source2-id', title: 'Mock Source 2', type: 'text' },
{ id: '123e4567-e89b-12d3-a456-426614174000' as UUID, title: 'Mock Source 1', type: 'text' },
{ id: '123e4567-e89b-12d3-a456-426614174001' as UUID, title: 'Mock Source 2', type: 'text' },
]),
};
}
Expand All @@ -60,7 +61,7 @@ describe('SourcesDisplay (with onAction-based SEARCH_SOURCES)', () => {
expect.objectContaining({ type: 'SEARCH_SOURCES', query: 'some query' }),
expect.any(Array), // messages
expect.any(Array), // sources
expect.any(Array), // activeSources
null, // activeSources can now be null
null // selectedSource on first usage is typically null
);

Expand All @@ -76,7 +77,7 @@ describe('SourcesDisplay (with onAction-based SEARCH_SOURCES)', () => {
if (action.type === 'SEARCH_SOURCES') {
return {
sources: Promise.resolve([
{ id: 'sourceA', title: 'Source A', type: 'text' },
{ id: '123e4567-e89b-12d3-a456-426614174002' as UUID, title: 'Source A', type: 'text' },
]),
};
}
Expand All @@ -97,7 +98,7 @@ describe('SourcesDisplay (with onAction-based SEARCH_SOURCES)', () => {
expect.objectContaining({ type: 'SEARCH_SOURCES', query: 'enter query' }),
expect.any(Array),
expect.any(Array),
expect.any(Array),
null, // activeSources can now be null
null
);
});
Expand All @@ -118,10 +119,14 @@ describe('SourcesDisplay (with onAction-based SEARCH_SOURCES)', () => {
// Return a single source
return {
sources: Promise.resolve([
{ id: 'source1', title: 'A Single Source', type: 'text' },
{ id: '123e4567-e89b-12d3-a456-426614174003' as UUID, title: 'A Single Source', type: 'text' },
]),
};
}
// Handle CLEAR_SOURCES action
if (action.type === 'CLEAR_SOURCES') {
return {}; // Return empty object for successful action
}
return undefined;
});

Expand Down Expand Up @@ -154,4 +159,98 @@ describe('SourcesDisplay (with onAction-based SEARCH_SOURCES)', () => {
expect(screen.getByText('No sources available')).toBeInTheDocument();
});
});

it('handles setting active sources to null', async () => {
const mockOnAction = vi.fn((action) => {
if (action.type === 'SEARCH_SOURCES') {
return {
sources: Promise.resolve([
{ id: '123e4567-e89b-12d3-a456-426614174004' as UUID, title: 'Source X', type: 'text' },
{ id: '123e4567-e89b-12d3-a456-426614174005' as UUID, title: 'Source Y', type: 'text' },
]),
};
}
if (action.type === 'SET_ACTIVE_SOURCES') {
// Return empty object for successful action
return {};
}
return undefined;
});

const wrapper = createWrapper(mockOnAction);
render(<SourcesDisplay />, { wrapper });

// Perform a search to get sources
const searchInput = screen.getByPlaceholderText('Search knowledge base...');
await act(async () => {
fireEvent.change(searchInput, { target: { value: 'test query' } });
fireEvent.click(screen.getByText('Search'));
});

// Wait for sources to appear
await waitFor(() => {
expect(screen.getByText('Source X')).toBeInTheDocument();
expect(screen.getByText('Source Y')).toBeInTheDocument();
});

// Verify that the sources are displayed correctly
expect(screen.getByText('Source X')).toBeInTheDocument();
expect(screen.getByText('Source Y')).toBeInTheDocument();
});

it('handles setting selected source to null', async () => {
const mockOnAction = vi.fn((action) => {
if (action.type === 'SEARCH_SOURCES') {
return {
sources: Promise.resolve([
{ id: '123e4567-e89b-12d3-a456-426614174006' as UUID, title: 'Source Z', type: 'text' },
]),
};
}
if (action.type === 'SET_SELECTED_SOURCE') {
// Verify that null can be passed as sourceId
if (action.sourceId === null || action.sourceId === '') {
return {
selectedSourceId: null
};
}
return {
selectedSourceId: action.sourceId
};
}
return undefined;
});

const wrapper = createWrapper(mockOnAction);
render(<SourcesDisplay />, { wrapper });

// Perform a search to get sources
const searchInput = screen.getByPlaceholderText('Search knowledge base...');
await act(async () => {
fireEvent.change(searchInput, { target: { value: 'test query' } });
fireEvent.click(screen.getByText('Search'));
});

// Wait for source to appear
await waitFor(() => {
expect(screen.getByText('Source Z')).toBeInTheDocument();
});

// Click on the source to select it
await act(async () => {
fireEvent.click(screen.getByText('Source Z'));
});

// Verify that SET_SELECTED_SOURCE was called with the correct sourceId
expect(mockOnAction).toHaveBeenCalledWith(
expect.objectContaining({
type: 'SET_SELECTED_SOURCE',
sourceId: '123e4567-e89b-12d3-a456-426614174006'
}),
expect.any(Array),
expect.any(Array),
null,
null
);
});
});
119 changes: 84 additions & 35 deletions lexio/lib/components/SourcesDisplay/SourcesDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ResetWrapper } from "../../utils/ResetWrapper";
import { useSources} from "../../hooks";
import { TrashIcon } from '@heroicons/react/24/outline';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import {addOpacity} from "../../utils/scaleFontSize.tsx";
import {addOpacity, scaleFontSize} from "../../utils/scaleFontSize.tsx";

export interface SourcesDisplayStyles extends React.CSSProperties {
backgroundColor?: string;
Expand Down Expand Up @@ -202,40 +202,69 @@ const SourcesDisplay: React.FC<SourcesDisplayProps> = ({
style={{
backgroundColor: source.id === selectedSourceId
? style.selectedSourceBackground
: activeSources.includes(source)
: activeSources && activeSources.includes(source)
? style.activeSourceBackground
: activeSources.length > 0
: activeSources && activeSources.length > 0
? style.inactiveSourceBackground
: style.inactiveSourceBackground,
borderColor: source.id === selectedSourceId
? style.selectedSourceBorderColor
: activeSources.includes(source)
: activeSources && activeSources.includes(source)
? style.selectedSourceBorderColor
: style.inactiveSourceBorderColor,
opacity: activeSources.length > 0 && !activeSources.includes(source) ? 0.6 : 1,
opacity: activeSources && activeSources.length > 0 && !activeSources.includes(source) ? 0.6 : 1,
borderRadius: style.borderRadius,
fontSize: style.fontSize,
}}
onClick={() => setSelectedSource(source.id)}
>
<div className="flex items-start justify-between">
<div className="overflow-hidden">
<p className="font-medium truncate" style={{ color: style.color }}>
{source.title}
</p>
{source.type && (
<span className="inline-block px-2 py-1 font-medium rounded-full mt-1" style={{
backgroundColor: style.sourceTypeBackground,
color: style.sourceTypeColor,
fontSize: `calc(${style.fontSize} * 0.75)` // todo: replace with utils func
<div className="overflow-hidden flex-1 mr-2">
<div className="flex items-center gap-2">
<p className="font-medium truncate" style={{
color: style.color,
fontSize: scaleFontSize(style.fontSize || '16px', 1.0)
}}>
{source.type}
</span>
{source.title}
</p>
{source.href && (
<a
href={source.href}
target="_blank"
rel="noopener noreferrer"
className="flex-shrink-0 p-1 rounded hover:bg-gray-100 transition-colors"
onClick={(e) => e.stopPropagation()}
title="Open source link"
style={{
color: style.buttonBackground,
}}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
)}
</div>
{source.description && (
<p className="text-gray-500 line-clamp-2" style={{
color: addOpacity(style.color || colors.text, 0.6),
fontSize: scaleFontSize(style.fontSize || '14px', 0.95)
}}>
{source.description}
</p>
)}
{showRelevanceScore && source.relevance !== undefined && (
<div className="mt-2 flex items-center">
<span style={{ color: addOpacity(style.color || colors.text, 0.6), fontSize: `calc(${style.fontSize} * 0.9)` }}>Relevance:</span>
<div className="ml-2 h-2 w-24 rounded-full" style={{ backgroundColor: style.metadataTagBackground }}>
<div className="mt-2 flex items-center group relative">
<span style={{
color: addOpacity(style.color || colors.text, 0.6),
fontSize: scaleFontSize(style.fontSize || '12px', 0.85),
}}>Relevance:</span>
<div
className="ml-2 h-2 w-24 rounded-full"
style={{
backgroundColor: style.metadataTagBackground,
}}
>
<div
className="h-2 rounded-full"
style={{
Expand All @@ -244,29 +273,49 @@ const SourcesDisplay: React.FC<SourcesDisplayProps> = ({
}}
/>
</div>
<div className="absolute bottom-full left-1/3 transform -translate-x-1 mb-1 px-2 py-1 rounded text-xs whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity duration-200"
style={{
backgroundColor: style.backgroundColor,
color: style.color,
border: `1px solid ${style.metadataTagBackground}`,
fontSize: scaleFontSize(style.fontSize || '12px', 0.75),
}}>
{Math.round(source.relevance * 100)}%
</div>
</div>
)}
</div>
{source.type && (
<span className="inline-block px-2 py-1 font-medium rounded-full flex-shrink-0" style={{
backgroundColor: style.sourceTypeBackground,
color: style.sourceTypeColor,
fontSize: scaleFontSize(style.fontSize || '12px', 0.8)
}}>
{source.type}
</span>
)}
</div>
{showMetadata && source.metadata && Object.keys(source.metadata).length > 0 && (
<div className="mt-2 pt-2 border-t" style={{ borderColor: style.inactiveSourceBorderColor }}>
<div className="pt-2 border-t" style={{ borderColor: style.inactiveSourceBorderColor }}>
<div className="flex flex-wrap gap-2">
{Object.entries(source.metadata).map(([key, value]) => (
<span
key={key}
className="inline-flex items-center px-2 py-1 rounded-md"
style={{
backgroundColor: style.metadataTagBackground,
color: style.metadataTagColor,
fontSize: `calc(${style.fontSize} * 0.75)`,
lineHeight: '1.2',
}}
>
{key}: {value}
</span>
))}
{Object.entries(source.metadata)
.filter(([key]) => typeof key === "string" && !key.startsWith("_"))
.map(([key, value]) => (
<span
key={key}
className="inline-flex items-center px-2 py-1 rounded-md"
style={{
backgroundColor: style.metadataTagBackground,
color: style.metadataTagColor,
fontSize: scaleFontSize(style.fontSize || '12px', 0.85),
lineHeight: '1.2',
}}
>
{key}: {value}
</span>
))}
</div>
</div>
</div>
)}
</li>
))}
Expand Down
12 changes: 6 additions & 6 deletions lexio/lib/hooks/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ export const useSources = (component: Component) => {
dispatch({type: 'CLEAR_SOURCES', source: component}, false);
};

const setActiveSources = (sourceIds: string[] | UUID[]) => {
dispatch({type: 'SET_ACTIVE_SOURCES', sourceIds, source: component}, false);
const setActiveSources = (sourceIds: string[] | UUID[] | null) => {
dispatch({type: 'SET_ACTIVE_SOURCES', sourceIds: sourceIds || [], source: component}, false);
};

const setSelectedSource = (sourceId: string | UUID) => {
dispatch({type: 'SET_SELECTED_SOURCE', sourceId, source: component}, false);
const setSelectedSource = (sourceId: string | UUID | null) => {
dispatch({type: 'SET_SELECTED_SOURCE', sourceId: sourceId || '', source: component}, false);
};

const setFilterSources = (filter: any) => {
Expand Down Expand Up @@ -79,8 +79,8 @@ export const useMessages = (component: Component) => {
* Set the active message in the chat window
* @param messageId
*/
const setActiveMessage = (messageId: string | UUID) => {
dispatch({type: 'SET_ACTIVE_MESSAGE', messageId, source: component}, false);
const setActiveMessage = (messageId: string | UUID | null) => {
dispatch({type: 'SET_ACTIVE_MESSAGE', messageId: messageId || '', source: component}, false);
};

const clearMessages = () => {
Expand Down
Loading

0 comments on commit 5ac75b4

Please sign in to comment.