TransHub / client /src /pages /SearchTexts.tsx
linguabot's picture
Upload folder using huggingface_hub
4f163ba verified
import React, { useState, useEffect } from 'react';
import {
MagnifyingGlassIcon,
GlobeAltIcon,
AcademicCapIcon,
FunnelIcon,
PencilIcon,
CheckCircleIcon,
XCircleIcon,
ClockIcon
} from '@heroicons/react/24/outline';
import { api } from '../services/api';
interface SearchFilters {
sourceLanguage: string;
}
interface UserSubmission {
_id: string;
transcreation: string;
explanation: string;
status: 'pending' | 'approved' | 'rejected' | 'submitted';
createdAt: string;
score?: number;
voteCounts?: {
1: number;
2: number;
3: number;
};
}
const SearchTexts: React.FC = () => {
const [filters, setFilters] = useState<SearchFilters>({
sourceLanguage: ''
});
const [isSearching, setIsSearching] = useState(false);
const [searchResults, setSearchResults] = useState<any[]>([]);
const [error, setError] = useState('');
const [translations, setTranslations] = useState<{[key: string]: string}>({});
const [userSubmissions, setUserSubmissions] = useState<{[key: string]: UserSubmission[]}>({});
const [loadingSubmissions, setLoadingSubmissions] = useState(false);
// Fetch user's submissions when examples are loaded
useEffect(() => {
if (searchResults.length > 0) {
fetchUserSubmissions();
}
}, [searchResults]);
const fetchUserSubmissions = async () => {
setLoadingSubmissions(true);
try {
const token = localStorage.getItem('token');
if (!token) return;
const response = await fetch('/api/submissions/my-submissions', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const submissionsByExample: {[key: string]: UserSubmission[]} = {};
data.submissions.forEach((submission: UserSubmission & { sourceTextId: any }) => {
const exampleId = submission.sourceTextId?._id || submission.sourceTextId;
if (exampleId) {
if (!submissionsByExample[exampleId]) {
submissionsByExample[exampleId] = [];
}
submissionsByExample[exampleId].push(submission);
}
});
setUserSubmissions(submissionsByExample);
}
} catch (error) {
console.error('Error fetching submissions:', error);
} finally {
setLoadingSubmissions(false);
}
};
const handleSearch = async () => {
setIsSearching(true);
setError('');
try {
const response = await api.post('/search/auto-search', filters);
setSearchResults(response.data.results || []);
} catch (error: any) {
console.error('Search error:', error);
if (error.response?.data?.error) {
setError(error.response.data.error);
} else {
setError('Search failed. Please try again.');
}
} finally {
setIsSearching(false);
}
};
const handleFilterChange = (field: keyof SearchFilters, value: string) => {
setFilters(prev => ({
...prev,
[field]: value
}));
// Clear error when user changes filters
if (error) setError('');
};
const handleTranslationChange = (id: string, value: string) => {
setTranslations(prev => ({
...prev,
[id]: value
}));
};
const handleTranscreate = async (id: string) => {
const translation = translations[id];
if (!translation || !translation.trim()) {
alert('Please enter a translation before submitting.');
return;
}
try {
const token = localStorage.getItem('token');
if (!token) {
alert('Please log in to submit translations.');
return;
}
const result = searchResults.find(r => r.id === id);
if (!result) {
alert('Example not found.');
return;
}
// Submit translation to backend
const response = await fetch('/api/submissions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sourceTextId: id,
targetCulture: result.sourceCulture,
targetLanguage: result.sourceLanguage === 'English' ? 'Chinese' : 'English',
transcreation: translation.trim(),
explanation: 'Practice translation submission',
isAnonymous: true
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Clear the translation input
setTranslations(prev => ({
...prev,
[id]: ''
}));
// Refresh user's submissions to show the new one
await fetchUserSubmissions();
alert('Translation submitted successfully! It will be available for voting.');
} catch (error) {
console.error('Error submitting translation:', error);
alert('Failed to submit translation. Please try again.');
}
};
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Practice</h1>
<p className="mt-2 text-gray-600">
In-class practice examples for puns and wordplay in English and Chinese
</p>
</div>
{/* Search Filters */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<div className="flex items-center mb-4">
<FunnelIcon className="h-5 w-5 text-indigo-400 mr-2" />
<h2 className="text-lg font-medium text-gray-900">Language Filter</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="sourceLanguage" className="block text-sm font-medium text-gray-700 mb-2">
Language
</label>
<select
id="sourceLanguage"
value={filters.sourceLanguage}
onChange={(e) => handleFilterChange('sourceLanguage', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
>
<option value="">All Languages</option>
<option value="English">English</option>
<option value="Chinese">Chinese</option>
</select>
</div>
</div>
<div className="mt-6">
<button
onClick={handleSearch}
disabled={isSearching}
className="w-full md:w-auto bg-indigo-600 text-white px-6 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{isSearching ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Loading...
</>
) : (
<>
<MagnifyingGlassIcon className="h-4 w-4 mr-2" />
Show Examples
</>
)}
</button>
</div>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Search Error</h3>
<div className="mt-2 text-sm text-red-700">{error}</div>
</div>
</div>
</div>
)}
{/* Search Results */}
{searchResults.length > 0 && (
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">
Practice Examples ({searchResults.length})
</h3>
</div>
</div>
<div className="divide-y divide-gray-200">
{searchResults.map((result) => (
<div key={result.id} className="p-6">
<div className="flex-1">
<div className="text-gray-700 mb-4 whitespace-pre-wrap text-lg leading-relaxed">
{result.content}
</div>
{/* Translation Input - Only show if user hasn't submitted yet */}
{loadingSubmissions ? (
<div className="mb-4 p-4 bg-gray-50 rounded-md">
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-indigo-600 mr-2"></div>
<span className="text-sm text-gray-600">Loading your submissions...</span>
</div>
</div>
) : (!userSubmissions[result.id] || userSubmissions[result.id].length === 0) ? (
<div className="mb-4">
<label htmlFor={`translation-${result.id}`} className="block text-sm font-medium text-gray-700 mb-2">
Your Translation:
</label>
<textarea
id={`translation-${result.id}`}
value={translations[result.id] || ''}
onChange={(e) => handleTranslationChange(result.id, e.target.value)}
placeholder="Enter your translation here..."
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
rows={3}
/>
</div>
) : (
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-md">
<div className="flex items-center">
<CheckCircleIcon className="h-5 w-5 text-green-500 mr-2" />
<span className="text-sm font-medium text-green-800">Translation Submitted</span>
</div>
<p className="text-sm text-green-600 mt-1">
You have already submitted a translation for this example.
</p>
</div>
)}
{/* Transcreate Button - Only show if user hasn't submitted yet */}
{!loadingSubmissions && (!userSubmissions[result.id] || userSubmissions[result.id].length === 0) && (
<button
onClick={() => handleTranscreate(result.id)}
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 flex items-center"
>
<PencilIcon className="h-4 w-4 mr-2" />
Transcreate It
</button>
)}
{/* User's Previous Translations */}
{!loadingSubmissions && userSubmissions[result.id] && userSubmissions[result.id].length > 0 && (
<div className="mt-6 border-t border-gray-200 pt-4">
<h4 className="text-sm font-medium text-gray-900 mb-3">Your Translation:</h4>
<div className="space-y-3">
{userSubmissions[result.id].map((submission) => (
<div key={submission._id} className="bg-indigo-50 rounded-lg p-4 border border-indigo-200">
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-gray-900 mb-2 font-medium">{submission.transcreation}</p>
<div className="flex items-center space-x-4 text-sm text-gray-500">
<span className="flex items-center">
{submission.status === 'approved' && (
<CheckCircleIcon className="h-4 w-4 text-green-500 mr-1" />
)}
{submission.status === 'rejected' && (
<XCircleIcon className="h-4 w-4 text-red-500 mr-1" />
)}
{submission.status === 'pending' && (
<ClockIcon className="h-4 w-4 text-yellow-500 mr-1" />
)}
{submission.status === 'submitted' && (
<ClockIcon className="h-4 w-4 text-blue-500 mr-1" />
)}
{submission.status.charAt(0).toUpperCase() + submission.status.slice(1)}
</span>
{submission.voteCounts && (
<span className="bg-gray-100 text-gray-800 px-2 py-1 rounded text-xs font-medium">
Votes: {submission.voteCounts[1] || 0} 1st, {submission.voteCounts[2] || 0} 2nd, {submission.voteCounts[3] || 0} 3rd
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* No Results */}
{searchResults.length === 0 && !isSearching && !error && (
<div className="bg-white rounded-lg shadow p-8 text-center">
<AcademicCapIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No examples found</h3>
<p className="text-gray-600 mb-4">
Try adjusting your language filter or click "Show Examples" to see all available practice examples.
</p>
<button
onClick={handleSearch}
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Show All Examples
</button>
</div>
)}
</div>
);
};
export default SearchTexts;