diff --git a/frontend/src/components/ResponsePanel.tsx b/frontend/src/components/ResponsePanel.tsx new file mode 100644 index 0000000..224743e --- /dev/null +++ b/frontend/src/components/ResponsePanel.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import { MessageSquare, AlertCircle } from 'lucide-react' +import type { SourceMetadata } from '../types' + +interface ResponsePanelProps { + answer: string | null + sources: SourceMetadata[] + isLoading: boolean + error: string | null +} + +export const ResponsePanel: React.FC = ({ + answer, + sources, + isLoading, + error, +}) => { + if (answer === null && !isLoading && !error) { + return ( +
+
+ +
Ask a question to see the answer here.
+
+
+ ) + } + + if (isLoading) { + return ( +
+
+
+
+
+ ) + } + + if (error) { + return ( +
+
+ + {error} +
+
+ ) + } + + return ( +
+
+ {answer + ?.split('\n') + .map((line, index) => { + const trimmedLine = line.trim() + if (trimmedLine.startsWith('-') || trimmedLine.startsWith('•')) { + const content = trimmedLine.replace(/^[-•]\s*/, '') + return ( +
  • + {content} +
  • + ) + } + return

    {trimmedLine}

    + })} +
    + + {sources.length > 0 && ( +
    +

    Sources

    +
    + {sources.map((source, index) => ( +
    +
    {source.filename}
    +
    {source.upload_date}
    +
    {source.content_summary}
    +
    Chunk {source.chunk_index}
    +
    + ))} +
    +
    + )} +
    + ) +} diff --git a/frontend/src/test/components/ResponsePanel.test.tsx b/frontend/src/test/components/ResponsePanel.test.tsx new file mode 100644 index 0000000..4fa167b --- /dev/null +++ b/frontend/src/test/components/ResponsePanel.test.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { ResponsePanel } from '../../components/ResponsePanel' +import type { SourceMetadata } from '../../types' + +describe('ResponsePanel', () => { + const mockSources: SourceMetadata[] = [ + { + filename: 'document1.pdf', + upload_date: '2024-01-15', + content_summary: 'Introduction to RAG systems', + chunk_index: 0, + }, + { + filename: 'document2.txt', + upload_date: '2024-01-16', + content_summary: 'Advanced retrieval techniques', + chunk_index: 1, + }, + ] + + it('shows empty state message when no answer and not loading', () => { + render() + expect(screen.getByText(/Ask a question to see the answer here/i)).toBeInTheDocument() + }) + + it('shows loading skeletons when isLoading is true', () => { + render() + const skeletonElements = screen.getAllByTestId('skeleton-line') + expect(skeletonElements).toHaveLength(3) + expect(skeletonElements[0]).toHaveClass('w-full') + expect(skeletonElements[1]).toHaveClass('w-4/5') + expect(skeletonElements[2]).toHaveClass('w-3/5') + }) + + it('shows error message when error prop is set', () => { + render( + + ) + expect(screen.getByText(/Failed to fetch answer/i)).toBeInTheDocument() + }) + + it('renders answer text as bullet points', () => { + const answer = `- First point\n- Second point\n• Third point\nPlain text line` + render() + expect(screen.getByText('First point')).toBeInTheDocument() + expect(screen.getByText('Second point')).toBeInTheDocument() + expect(screen.getByText('Third point')).toBeInTheDocument() + expect(screen.getByText('Plain text line')).toBeInTheDocument() + }) + + it('renders source metadata cards', () => { + render( + + ) + expect(screen.getByText('document1.pdf')).toBeInTheDocument() + expect(screen.getByText('document2.txt')).toBeInTheDocument() + expect(screen.getByText('2024-01-15')).toBeInTheDocument() + expect(screen.getByText('Introduction to RAG systems')).toBeInTheDocument() + expect(screen.getByText('Advanced retrieval techniques')).toBeInTheDocument() + }) + + it('does not show sources section when sources array is empty', () => { + render() + expect(screen.queryByText(/sources/i)).not.toBeInTheDocument() + }) +})