Skip to content
Merged
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
Expand Up @@ -22,6 +22,7 @@ import { useNotification } from '../../contexts/NotificationContext';
// Import the API client and types
import apiClient, { Collection, CollectionDocument } from '../../services/apiClient';
import PodcastGenerationModal from '../podcasts/PodcastGenerationModal';
import SuggestedQuestions from './SuggestedQuestions';

// Use CollectionDocument type from apiClient instead of local CollectionFile
type CollectionFile = CollectionDocument;
Expand Down Expand Up @@ -242,6 +243,22 @@ const LightweightCollectionDetail: React.FC = () => {
setFilesToUpload([]);
};

const handleSuggestedQuestionClick = (question: string) => {
// Navigate to RAG search page with the collection and question
if (collection?.status === 'ready' || collection?.status === 'completed') {
navigate('/search', {
state: {
collectionId: collection.id,
collectionName: collection.name,
collectionDescription: collection.description,
initialQuery: question
}
});
} else {
addNotification('warning', 'Collection Not Ready', 'This collection is not ready for searching yet.');
}
};

const filteredDocuments = collection?.documents.filter(doc =>
doc.name.toLowerCase().includes(searchQuery.toLowerCase())
) || [];
Expand Down Expand Up @@ -345,6 +362,14 @@ const LightweightCollectionDetail: React.FC = () => {
</div>
</div>

{/* Suggested Questions */}
<div className="mb-6">
<SuggestedQuestions
collectionId={collection.id}
onQuestionClick={handleSuggestedQuestionClick}
/>
</div>

{/* Documents Table */}
<div className="card">
<div className="p-6 border-b border-gray-20">
Expand Down
151 changes: 151 additions & 0 deletions frontend/src/components/collections/SuggestedQuestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { useState, useEffect, useCallback } from 'react';
import apiClient, { SuggestedQuestion } from '../../services/apiClient';
import { useNotification } from '../../contexts/NotificationContext';
import { LightBulbIcon, ArrowPathIcon } from '@heroicons/react/24/outline';

interface SuggestedQuestionsProps {
collectionId: string;
onQuestionClick: (question: string) => void;
}

const SuggestedQuestions: React.FC<SuggestedQuestionsProps> = ({ collectionId, onQuestionClick }) => {
const [questions, setQuestions] = useState<SuggestedQuestion[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const { addNotification } = useNotification();


useEffect(() => {
let isMounted = true;

const fetchQuestionsWithCleanup = async () => {
if (!isMounted) return;

setIsLoading(true);
setError(null);
try {
const fetchedQuestions = await apiClient.getSuggestedQuestions(collectionId);
if (isMounted) {
setQuestions(fetchedQuestions);
}
} catch (err) {
if (isMounted) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load suggested questions.';
console.error('Error fetching suggested questions:', err);
setError(errorMessage);
addNotification('error', 'Error', `Could not fetch suggested questions: ${errorMessage}`);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};

fetchQuestionsWithCleanup();

return () => {
isMounted = false;
};
}, [collectionId, addNotification]);

const handleRefresh = async () => {
setIsRefreshing(true);
try {
const fetchedQuestions = await apiClient.getSuggestedQuestions(collectionId);
setQuestions(fetchedQuestions);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load suggested questions.';
console.error('Error fetching suggested questions:', err);
setError(errorMessage);
addNotification('error', 'Error', `Could not fetch suggested questions: ${errorMessage}`);
} finally {
setIsRefreshing(false);
}
};

if (isLoading) {
return (
<div className="p-4 bg-gray-20 rounded-lg animate-pulse">
<div className="h-4 bg-gray-30 rounded w-1/4 mb-2"></div>
<div className="flex flex-wrap gap-2">
<div className="h-8 bg-gray-30 rounded-full w-32"></div>
<div className="h-8 bg-gray-30 rounded-full w-48"></div>
<div className="h-8 bg-gray-30 rounded-full w-40"></div>
</div>
</div>
);
}

if (error) {
return (
<div className="p-4 bg-red-10 border border-red-20 rounded-lg text-red-70">
<p>{error}</p>
<button
onClick={handleRefresh}
className="mt-2 px-3 py-1 bg-red-50 text-white rounded-md hover:bg-red-60 text-sm"
>
Try Again
</button>
</div>
);
}

if (questions.length === 0) {
return (
<div className="p-4 bg-gray-20 rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center text-sm font-semibold text-gray-80">
<LightBulbIcon className="w-5 h-5 mr-2" />
<span>Suggested Questions</span>
</div>
<button
onClick={handleRefresh}
disabled={isRefreshing}
className={`p-1 text-gray-60 hover:text-gray-90 ${isRefreshing ? 'animate-spin' : ''}`}
title="Refresh suggested questions"
aria-label="Refresh suggested questions"
>
<ArrowPathIcon className="w-4 h-4" />
</button>
</div>
<p className="text-sm text-gray-60">No suggested questions available at the moment. Questions will be generated automatically after document processing is complete.</p>
</div>
);
}

return (
<div className="p-4 bg-gray-20 rounded-lg">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center text-sm font-semibold text-gray-80">
<LightBulbIcon className="w-5 h-5 mr-2" />
<span>Suggested Questions</span>
</div>
<button
onClick={handleRefresh}
disabled={isRefreshing}
className={`p-1 text-gray-60 hover:text-gray-90 ${isRefreshing ? 'animate-spin' : ''}`}
title="Refresh suggested questions"
aria-label="Refresh suggested questions"
>
<ArrowPathIcon className="w-4 h-4" />
</button>
</div>
<div className="flex flex-wrap gap-2">
{questions.map((q) => (
<button
key={q.id}
onClick={() => onQuestionClick(q.question)}
className="px-3 py-1.5 bg-blue-10 text-blue-70 rounded-full hover:bg-blue-20 text-sm transition-colors"
aria-label={`Use suggested question: ${q.question}`}
>
{q.question}
</button>
))}
</div>
</div>
);
};

export default SuggestedQuestions;
15 changes: 15 additions & 0 deletions frontend/src/services/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ interface PodcastQuestionInjection {
user_id: string;
}

interface SuggestedQuestion {
id: string;
collection_id: string;
question: string;
created_at: string;
}

class ApiClient {
private client: AxiosInstance;

Expand Down Expand Up @@ -403,6 +410,13 @@ class ApiClient {
await this.client.delete(`/api/collections/${id}`);
}

async getSuggestedQuestions(collectionId: string): Promise<SuggestedQuestion[]> {
const response: AxiosResponse<SuggestedQuestion[]> = await this.client.get(
`/api/collections/${collectionId}/questions`
);
return response.data;
}

// Document API
async uploadDocuments(collectionId: string, files: File[]): Promise<CollectionDocument[]> {
const formData = new FormData();
Expand Down Expand Up @@ -921,5 +935,6 @@ export type {
PodcastQuestionInjection,
VoiceSettings,
PodcastStepDetails,
SuggestedQuestion,
VoiceId,
};
Loading