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
23 changes: 22 additions & 1 deletion src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,28 @@ export const Footer = ({translucent = false, className = ''}: FooterProps) => {
<footer className={fullClass}>
<div className="px-4 sm:px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div className="hidden md:block md:flex-1"></div>
{/* Left: e-INFRA CZ Acknowledgement */}
<div className="flex-1 flex flex-col items-center md:items-start gap-2">
<a
href="https://www.e-infra.cz/en"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
aria-label="e-INFRA CZ website"
>
<img
src="https://www.e-infra.cz/img/logo.svg"
alt="e-INFRA CZ"
className="h-8 w-auto"
/>
</a>
<div className="text-xs font-light text-center md:text-left max-w-xs">
<p className="text-[10px] text-gray-500">
Computational resources provided by e-INFRA CZ (ID:90254), supported by Ministry of
Education, Youth and Sports of the Czech Republic
</p>
</div>
</div>

{/* Center text */}
<div className="text-center md:flex-1">
Expand Down
80 changes: 65 additions & 15 deletions src/components/SearchResultItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {CitationExport} from './CitationExport';

interface SearchResultItemProps {
hit: BackendDataset;
isAiRanked?: boolean;
}

export const SearchResultItem = ({hit}: SearchResultItemProps) => {
export const SearchResultItem = ({hit, isAiRanked = false}: SearchResultItemProps) => {
const cleanDescription = (html: string) => {
const div = document.createElement('div');
div.innerHTML = html;
Expand All @@ -17,18 +18,65 @@ export const SearchResultItem = ({hit}: SearchResultItemProps) => {

const scorePercent = (hit.score || 0) * 100;

// For research accuracy, only use official publication dates (Issued/Created), not metadata dates
const getPublicationDate = (): string | null => {
// First priority: root-level publicationDate field (if not null)
if (hit.publicationDate) {
return hit.publicationDate;
}

// Second priority: look for "Issued" date in _source.dates (official publication)
if (hit._source.dates && hit._source.dates.length > 0) {
const issuedDate = hit._source.dates.find(d => d.dateType === 'Issued');
if (issuedDate) return issuedDate.date;

// Third priority: "Available" date (when it became publicly available)
const availableDate = hit._source.dates.find(d => d.dateType === 'Available');
if (availableDate) return availableDate.date;

// Fourth priority: "Created" date (when content was created)
// This is important for unpublished datasets - the creation date is citation-worthy
const createdDate = hit._source.dates.find(d => d.dateType === 'Created');
if (createdDate) return createdDate.date;
}

// Last resort: use publicationYear if available (show just the year)
if (hit._source.publicationYear) {
return hit._source.publicationYear;
}

return null;
};

const publicationDate = getPublicationDate();

const formatDate = (dateStr: string): string => {
// If it's just a year (4 digits), return as-is
if (/^\d{4}$/.test(dateStr)) {
return dateStr;
}
// Otherwise format as YYYY.MM.DD
return new Date(dateStr).toISOString().slice(0, 10).replace(/-/g, '.');
};

return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
<div className={`rounded-lg shadow-sm border p-6 hover:shadow-md transition-shadow ${
isAiRanked
? 'bg-white border-gray-200'
: 'bg-gray-100 border-gray-300'
}`}>
<div className="flex flex-col sm:flex-row justify-between items-start mb-3">
<h3 className="text-lg font-semibold text-gray-900 pr-4 mb-2 sm:mb-0">
{hit.title}
</h3>
<div className="flex-shrink-0 flex items-center space-x-1 bg-yellow-50 px-2 py-1 rounded-full">
<ProportionalStar percent={scorePercent} className="h-4 w-4"/>
<span className="text-sm font-medium text-yellow-700">
{scorePercent.toFixed(0)}%
</span>
</div>
{isAiRanked && hit.score !== undefined && (
<div className="flex-shrink-0 flex items-center space-x-1 bg-yellow-50 px-2 py-1 rounded-full">
<ProportionalStar percent={scorePercent} className="h-4 w-4"/>
<span className="text-sm font-medium text-yellow-700">
{scorePercent.toFixed(0)}%
</span>
</div>
)}
</div>

<p className="text-gray-700 mb-4 leading-relaxed">
Expand All @@ -38,26 +86,26 @@ export const SearchResultItem = ({hit}: SearchResultItemProps) => {
<div className="space-y-2 mb-4">
{hit._source.creators?.length > 0 && (
<div className="flex items-center space-x-2">
<UserIcon className="h-4 w-4 text-gray-500"/>
<UserIcon className="h-4 w-4 text-gray-500 flex-shrink-0"/>
<span className="text-sm text-gray-600">
{hit._source.creators.map(creator => creator.creatorName).slice(0, 3).join(', ')}
{hit._source.creators.length > 3 && ` +${hit._source.creators.length - 3} more`}
</span>
</div>
)}

{hit.publicationDate &&
{publicationDate && (
<div className="flex items-center space-x-2">
<CalendarIcon className="h-4 w-4 text-gray-500"/>
<CalendarIcon className="h-4 w-4 text-gray-500 flex-shrink-0"/>
<span className="text-sm text-gray-600">
{new Date(hit.publicationDate).toISOString().slice(0, 10).replace(/-/g, '.')}
{formatDate(publicationDate)}
</span>
</div>
}
)}

{hit._source.subjects && hit._source.subjects.length > 0 && (
<div className="flex items-start space-x-2">
<TagIcon className="h-4 w-4 text-gray-500 mt-0.5"/>
<TagIcon className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5"/>
<div className="flex flex-wrap gap-1">
{hit._source.subjects.slice(0, 5).map((subj, index) => (
<span key={index}
Expand Down Expand Up @@ -86,7 +134,9 @@ export const SearchResultItem = ({hit}: SearchResultItemProps) => {
</a>
<CitationExport dataset={hit}/>
</div>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">AI-powered search</span>
{isAiRanked && (
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">AI-powered search</span>
)}
</div>
</div>
);
Expand Down
58 changes: 48 additions & 10 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ export interface SSEEvent {
tool_call_id?: string;
content?: string;
role?: string;
error?: string; // For RUN_ERROR events
timestamp?: string | null;
raw_event?: unknown;
}

export interface SSEEventHandler {
onSearchData?: (data: BackendSearchResponse) => void;
onRerankedData?: (data: BackendSearchResponse) => void;
onEvent?: (event: SSEEvent) => void;
onError?: (error: Error) => void;
}

export const searchWithBackend = async (
query: string,
model: string = 'einfracz/qwen3-coder',
model: string = 'einfracz/gpt-oss-120b',
handlers: SSEEventHandler
): Promise<BackendSearchResponse> => {
const requestBody: SearchRequest = {
Expand All @@ -50,16 +54,31 @@ export const searchWithBackend = async (
// Handle SSE stream based on tool_call_id
return handleStream(response, (event) => {
if (handlers?.onEvent) handlers.onEvent(event);

// Handle RUN_ERROR - unrecoverable error during agent run
if (event.type === 'RUN_ERROR') {
const errorMessage = event.error || event.content || 'Agent run failed';
const error = new Error(errorMessage);
if (handlers?.onError) handlers.onError(error);
throw error; // Terminate stream processing
}

// Handle TOOL_CALL_RESULT events
if (event.type === 'TOOL_CALL_RESULT' && event.content) {
const searchResp = JSON.parse(event.content) as BackendSearchResponse;
if (event.tool_call_id === 'rerank_results')
handlers.onSearchData(searchResp);
else if (event.tool_call_id === 'search_data')
handlers.onSearchData(searchResp);
if (event.tool_call_id === 'rerank_results') {
if (handlers.onRerankedData) handlers.onRerankedData(searchResp);
} else if (event.tool_call_id === 'search_data') {
if (handlers.onSearchData) handlers.onSearchData(searchResp);
}
return searchResp;
} else if (event.type === 'error' && handlers?.onError) {
handlers.onError(new Error(event.content));
}

// Legacy error handling (backward compatibility)
if (event.type === 'error' && handlers?.onError) {
handlers.onError(new Error(event.content || 'Unknown error'));
}

return null;
});
} catch (error) {
Expand All @@ -81,6 +100,7 @@ const handleStream = async (
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
let latestResults: BackendSearchResponse | null = null;
let runError: Error | null = null;

try {
while (true) {
Expand All @@ -95,13 +115,31 @@ const handleStream = async (
const dataLine = part.split('\n').find(line => line.startsWith('data:'));
if (!dataLine) continue;
const event = JSON.parse(dataLine.slice(5).trim()) as SSEEvent;
const result = onMessage(event);
if (result) latestResults = result;
try {
const result = onMessage(event);
if (result) latestResults = result;
} catch (e) {
// RUN_ERROR thrown from onMessage handler
if (e instanceof Error) {
runError = e;
break; // Stop processing further events
}
throw e;
}
} catch (e) {
logError(e, 'Failed to parse SSE event');
// Only log parse errors, not runtime errors
if (!runError) {
logError(e, 'Failed to parse SSE event');
}
}
}
// Break outer loop if we encountered a RUN_ERROR
if (runError) break;
}

// If we encountered a RUN_ERROR, throw it
if (runError) throw runError;

if (!latestResults) throw new Error('No search results received');
return latestResults;
} finally {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const LandingPage = () => {
{dataCards.map((card, index) => (
<div
key={index}
onClick={() => handleSearch(card, "einfracz/qwen3-coder")}
onClick={() => handleSearch(card, "einfracz/gpt-oss-120b")}
className="bg-white border border-eosc-border rounded-xl p-6 min-h-[75px] flex items-center justify-center cursor-pointer hover:bg-gray-50 hover:border-eosc-light-blue transition-colors"
>
<p className="text-sm font-light text-black text-center">
Expand Down
Loading