Skip to content

Commit 2da1810

Browse files
committed
Add paper body search functionality with component-based refactoring
1 parent a43264a commit 2da1810

8 files changed

Lines changed: 371 additions & 135 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
const {match, query}: {match: string, query: string} = $props();
3+
4+
let matchWords = $derived(
5+
query.split(' ')
6+
.filter(chunk => chunk.trim().length > 2)
7+
.map(word => word.toLowerCase())
8+
.filter((word, index, arr) => arr.indexOf(word) === index)
9+
);
10+
11+
let highlightedText = $derived(match.split(' ')
12+
.map(chunk => {
13+
if (matchWords.includes(chunk.toLowerCase())) {
14+
return `<span class="datailama-match">${chunk}</span>`
15+
} else {
16+
return chunk;
17+
}
18+
}).join(' ')
19+
)
20+
</script>
21+
22+
<style>
23+
:global(.datailama-match) {
24+
font-weight: bold;
25+
color: #2563eb;
26+
}
27+
</style>
28+
29+
<div class="datailama-match-formatter">
30+
{@html highlightedText}
31+
</div>
32+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<script lang="ts">
2+
import type { Paper } from '$lib/models/paper';
3+
4+
const {
5+
onPapers = (papers: Paper[], count: number) => {}
6+
} = $props();
7+
8+
let isLoading = $state(false);
9+
let isDirty = $state(false);
10+
let prompt = $state('');
11+
let debounceTimer = $state<number | null>(null);
12+
13+
function debouncedSearch() {
14+
// Clear any existing timer
15+
if (debounceTimer !== null) {
16+
clearTimeout(debounceTimer);
17+
}
18+
19+
// Set isLoading immediately to show feedback
20+
isLoading = true;
21+
22+
// Set a new timer
23+
debounceTimer = setTimeout(() => {
24+
search();
25+
debounceTimer = null;
26+
}, 1000); // 1 second debounce
27+
}
28+
29+
async function search() {
30+
isLoading = true;
31+
32+
const requestUrl = `/api/paper/search/body?prompt=${encodeURIComponent(prompt)}&limit=10`;
33+
console.log(requestUrl);
34+
const response = await fetch(requestUrl);
35+
const data = await response.json();
36+
console.log(data);
37+
38+
if (data.count && data.count > 0) {
39+
onPapers(data.paper, data.count, prompt);
40+
} else {
41+
onPapers([], 0, prompt);
42+
}
43+
44+
isLoading = false;
45+
}
46+
</script>
47+
48+
<div class="bg-white rounded-lg shadow-md">
49+
<div class="w-full p-4 flex items-center justify-between">
50+
<div class="flex-1">
51+
<input
52+
type="text"
53+
name="prompt"
54+
id="prompt"
55+
class="w-full text-lg rounded-md border-gray-300 focus:border-gray-500"
56+
bind:value={prompt}
57+
oninput={debouncedSearch}
58+
placeholder="Search articles..."
59+
/>
60+
</div>
61+
<div class="flex items-center gap-4 ml-4">
62+
{#if isLoading}
63+
<svg class="animate-spin h-5 w-5 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
64+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
65+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
66+
</svg>
67+
{/if}
68+
</div>
69+
</div>
70+
</div>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<script lang="ts">
2+
import type { Paper } from '$lib/models/paper';
3+
import MatchFormatter from './MatchFormatter.svelte';
4+
5+
// Props
6+
const { paper, query }: { paper: Paper, query: string } = $props();
7+
8+
// State
9+
let isExpanded = $state(false);
10+
11+
function toggleExpand() {
12+
isExpanded = !isExpanded;
13+
}
14+
</script>
15+
16+
<div class="bg-white rounded-lg shadow-md p-4 mb-4 hover:shadow-lg transition-shadow">
17+
<!-- Match text if available -->
18+
{#if paper.match}
19+
<div class="flex flex-col text-gray-800 text-sm mb-3 p-3 bg-gray-50 rounded-md border-l-4 border-blue-400">
20+
<MatchFormatter match={paper.match} {query} />
21+
</div>
22+
{/if}
23+
24+
<!-- Preview mode - Collapsed -->
25+
{#if !isExpanded}
26+
<div class="flex justify-between items-center">
27+
<div class="flex-1">
28+
<h3 class="text-lg font-semibold text-gray-900 truncate">{paper.title}</h3>
29+
<p class="text-sm text-gray-600">{paper.author} et al. • {new Date(paper.published).getFullYear()}</p>
30+
</div>
31+
<button
32+
class="ml-2 p-2 text-gray-500 hover:bg-gray-100 rounded-full transition-colors"
33+
onclick={toggleExpand}
34+
aria-label="Expand details"
35+
>
36+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
37+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
38+
</svg>
39+
</button>
40+
</div>
41+
{:else}
42+
<!-- Full details - Expanded -->
43+
<div class="mb-2 flex justify-between">
44+
<div class="flex-1">
45+
<h3 class="text-lg font-semibold text-gray-900 mb-1">{paper.title}</h3>
46+
</div>
47+
<button
48+
class="ml-2 p-2 text-gray-500 hover:bg-gray-100 rounded-full transition-colors"
49+
onclick={toggleExpand}
50+
aria-label="Collapse details"
51+
>
52+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
53+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
54+
</svg>
55+
</button>
56+
</div>
57+
58+
<div class="flex flex-col lg:flex-row gap-4">
59+
<!-- Left column: DOI and Journal link -->
60+
<div class="lg:w-1/5 flex flex-col">
61+
{#if paper.doi}
62+
<a href="https://doi.org/{paper.doi}" target="_blank" rel="noopener noreferrer"
63+
class="inline-block bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full mb-2">
64+
DOI: {paper.doi}
65+
</a>
66+
{/if}
67+
{#if paper.journal}
68+
<a href={paper.url} target="_blank" rel="noopener noreferrer"
69+
class="text-sm text-gray-600 hover:text-blue-600">
70+
{paper.journal}&nbsp;
71+
</a>
72+
{/if}
73+
</div>
74+
75+
<!-- Middle column: Author -->
76+
<div class="lg:w-3/5">
77+
<p class="text-sm text-gray-600">{paper.author} et al.</p>
78+
</div>
79+
80+
<!-- Right column: Year and Citations -->
81+
<div class="lg:w-1/5 text-right">
82+
<div class="text-sm font-medium text-gray-500">{new Date(paper.published).getFullYear()}</div>
83+
<div class="text-xl font-bold text-gray-900">{paper.citations} / {paper.citations_year.toFixed(2)}</div>
84+
<div class="text-xs text-gray-500">citations / per year</div>
85+
</div>
86+
</div>
87+
{/if}
88+
</div>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<script lang="ts">
2+
import type { Paper } from '$lib/models/paper';
3+
4+
// Props with callbacks
5+
const {
6+
initialTitle = '',
7+
initialAuthor = '',
8+
initialOrder = 'citations_year',
9+
initialDirection = 'desc',
10+
onPapers = (papers: Paper[], count: number) => {}
11+
} = $props();
12+
13+
// State
14+
let title = $state(initialTitle);
15+
let author = $state(initialAuthor);
16+
let order = $state(initialOrder);
17+
let direction = $state(initialDirection);
18+
let isLoading = $state(false);
19+
let isExpanded = $state(false);
20+
21+
async function search() {
22+
isLoading = true;
23+
const requestUrl = `/api/paper/search/title?title=${encodeURIComponent(title)}&author=${encodeURIComponent(author)}&order=${encodeURIComponent(order)}&direction=${encodeURIComponent(direction)}&limit=10`;
24+
console.log(requestUrl);
25+
const response = await fetch(requestUrl);
26+
const data = await response.json();
27+
console.log(data);
28+
29+
// Call the callback with search results
30+
if (data.count && data.count > 0) {
31+
onPapers(data.paper, data.count);
32+
} else {
33+
onPapers([], 0);
34+
}
35+
36+
isLoading = false;
37+
}
38+
</script>
39+
40+
<div class="bg-white rounded-lg shadow-md">
41+
<!-- Basic Search Header -->
42+
<div class="w-full p-4 flex items-center justify-between">
43+
<div class="flex-1">
44+
<input
45+
type="text"
46+
name="title"
47+
id="title"
48+
class="w-full text-lg rounded-md border-gray-300 focus:border-gray-500"
49+
bind:value={title}
50+
oninput={search}
51+
placeholder="Search by title..."
52+
/>
53+
</div>
54+
<div class="flex items-center gap-4 ml-4">
55+
{#if isLoading}
56+
<svg class="animate-spin h-5 w-5 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
57+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
58+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
59+
</svg>
60+
{/if}
61+
<button
62+
class="p-2 hover:bg-gray-50 rounded-full transition-colors"
63+
onclick={() => isExpanded = !isExpanded}
64+
aria-label={isExpanded ? "Collapse advanced search" : "Expand advanced search"}
65+
>
66+
<svg
67+
class="w-6 h-6 text-gray-500 transform transition-transform {isExpanded ? 'rotate-180' : ''}"
68+
xmlns="http://www.w3.org/2000/svg"
69+
viewBox="0 0 20 20"
70+
fill="currentColor"
71+
>
72+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
73+
</svg>
74+
</button>
75+
</div>
76+
</div>
77+
78+
<!-- Advanced Search Content -->
79+
{#if isExpanded}
80+
<div class="border-t border-gray-200 p-4">
81+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
82+
<div>
83+
<label for="author" class="block text-sm font-medium text-gray-700">Author</label>
84+
<input
85+
type="text"
86+
name="author"
87+
id="author"
88+
class="mt-1 block w-full text-lg rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
89+
bind:value={author}
90+
oninput={search}
91+
/>
92+
</div>
93+
<div>
94+
<label for="order" class="block text-sm font-medium text-gray-700">Order by</label>
95+
<select
96+
name="order"
97+
id="order"
98+
class="mt-1 block w-full text-lg rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
99+
bind:value={order}
100+
onchange={search}
101+
>
102+
<option value="citations_year">Citations per Year</option>
103+
<option value="citations">Total Citations</option>
104+
</select>
105+
</div>
106+
<div>
107+
<label for="direction" class="block text-sm font-medium text-gray-700">Direction</label>
108+
<select
109+
name="direction"
110+
id="direction"
111+
class="mt-1 block w-full text-lg rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
112+
bind:value={direction}
113+
onchange={search}
114+
>
115+
<option value="desc">Descending</option>
116+
<option value="asc">Ascending</option>
117+
</select>
118+
</div>
119+
</div>
120+
</div>
121+
{/if}
122+
</div>

frontend/src/lib/models/paper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ export interface Paper {
88
published: Date;
99
citations: number;
1010
citations_year: number;
11+
match?: string;
12+
cosine_distance?: number;
1113
}

0 commit comments

Comments
 (0)