@meaningfully/ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +3 -0
  3. package/dist/App.svelte +86 -0
  4. package/dist/App.svelte.d.ts +7 -0
  5. package/dist/assets/base.css +56 -0
  6. package/dist/assets/electron.svg +10 -0
  7. package/dist/assets/main.css +171 -0
  8. package/dist/components/ApiKeyPage.svelte +128 -0
  9. package/dist/components/ApiKeyPage.svelte.d.ts +9 -0
  10. package/dist/components/ApiKeyStatus.svelte +38 -0
  11. package/dist/components/ApiKeyStatus.svelte.d.ts +7 -0
  12. package/dist/components/CsvUpload.svelte +101 -0
  13. package/dist/components/CsvUpload.svelte.d.ts +5 -0
  14. package/dist/components/DatabaseConfig.svelte +485 -0
  15. package/dist/components/DatabaseConfig.svelte.d.ts +6 -0
  16. package/dist/components/ExistingDatabases.svelte +175 -0
  17. package/dist/components/ExistingDatabases.svelte.d.ts +11 -0
  18. package/dist/components/FrontPage.svelte +18 -0
  19. package/dist/components/FrontPage.svelte.d.ts +8 -0
  20. package/dist/components/HelpPage.svelte +119 -0
  21. package/dist/components/HelpPage.svelte.d.ts +18 -0
  22. package/dist/components/Preview.svelte +37 -0
  23. package/dist/components/Preview.svelte.d.ts +9 -0
  24. package/dist/components/Results.svelte +121 -0
  25. package/dist/components/Results.svelte.d.ts +10 -0
  26. package/dist/components/SearchPage.svelte +256 -0
  27. package/dist/components/SearchPage.svelte.d.ts +9 -0
  28. package/dist/components/Table.svelte +87 -0
  29. package/dist/components/Table.svelte.d.ts +11 -0
  30. package/dist/env.d.ts +39 -0
  31. package/dist/index.d.ts +13 -0
  32. package/dist/index.js +14 -0
  33. package/dist/stores/fileDataStore.d.ts +8 -0
  34. package/dist/stores/fileDataStore.js +3 -0
  35. package/dist/style.css +1 -0
  36. package/dist/types.d.ts +60 -0
  37. package/dist/types.js +1 -0
  38. package/package.json +74 -0
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import ExistingDatabases from './ExistingDatabases.svelte'
3
+ import CsvUpload from './CsvUpload.svelte'
4
+ import type { MeaningfullyAPI } from '../types';
5
+
6
+ interface Props {
7
+ validApiKeysSet: boolean;
8
+ api: MeaningfullyAPI;
9
+ }
10
+
11
+ let { validApiKeysSet, api }: Props = $props();
12
+ </script>
13
+
14
+ <div class="container mx-auto px-4 space-y-8">
15
+ <CsvUpload validApiKeysSet={validApiKeysSet} />
16
+ <ExistingDatabases api={api} />
17
+ </div>
18
+
@@ -0,0 +1,8 @@
1
+ import type { MeaningfullyAPI } from '../types';
2
+ interface Props {
3
+ validApiKeysSet: boolean;
4
+ api: MeaningfullyAPI;
5
+ }
6
+ declare const FrontPage: import("svelte").Component<Props, {}, "">;
7
+ type FrontPage = ReturnType<typeof FrontPage>;
8
+ export default FrontPage;
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ </script>
3
+
4
+
5
+ <div class="container mx-auto px-4 space-y-8">
6
+ <h1>Help</h1>
7
+ <section>
8
+ <h2>What is Semantic Search?</h2>
9
+ <p>
10
+ Keyword search has been the only kind of search for decades. Sometimes it fails:
11
+ </p>
12
+
13
+ <ul>
14
+ <li>Ambiguity and synonyms (people getting fired and things catching on fire, cars and automobiles)</li>
15
+ <li>Circumlocutions and legalese</li>
16
+ <li>Documents written by laypeople describing complex situations in widely-varied language</li>
17
+ <li>Typos</li>
18
+ <li>Multilingual documents</li>
19
+ </ul>
20
+ <p>
21
+ Semantic search works better in these situations, by finding results that <i>mean</i> something
22
+ similar to the query. Even if the words are completely different, semantic search can still
23
+ surface the results you need.
24
+ </p>
25
+ </section>
26
+ <section>
27
+ <h2> a CSV</h2>
28
+ <p>
29
+ You can select a CSV from your computer to "upload" it to Meaningfully. Then, select one column
30
+ from the CSV to search semantically, and any number of other columns to be shown alongside it in results.
31
+ </p>
32
+ <p>
33
+ Once you upload the CSV, each entry in the chosen column will be embedded, with the results stored on your
34
+ computer. If you choose a remote embedding API -- like OpenAI's text-embedding-small or text-embedding-large --
35
+ then the entries in your column will be sent
36
+ to that service; if you choose a local one, then the data will not leave your computer.
37
+ </p>
38
+ <p>
39
+ Meaningfully provides additional options:
40
+ </p>
41
+ <ul>
42
+ <li>Split long text into sentences. When a single row contains many ideas, splitting it by sentence helps the search surface
43
+ results that match a single idea.
44
+ </li>
45
+ <li>Combine short sentences into chunks. You can adjust how large the chunks are and how much overlap exists between chunks.</li>
46
+ </ul>
47
+ <p>
48
+ Embedding can take a while, especially for large CSVs with 10,000 or more rows. Once it finishes, you'll
49
+ be able to search your CSV.
50
+ </p>
51
+ </section>
52
+ <section>
53
+ <h2>How should I write my search query?</h2>
54
+ <p>
55
+ <strong>Do: </strong> Imagine the perfect version of what you're looking for, that you wish exists in your spreadsheet. <span class="block whitespace-pre overflow-x-scroll">My car caught on fire as I was driving on the highway.</span>
56
+ </p>
57
+ <p>
58
+ <strong>Don't: </strong> Just write keywords. <span class="block whitespace-pre overflow-x-scroll">crash OR fire OR aflame</span>
59
+ </p>
60
+ <p>
61
+ <strong>Don't: </strong> Ask a question, like you would to a chatbot. <span class="block whitespace-pre overflow-x-scroll">Please find me examples about cars catching on fire.</span>
62
+ </p>
63
+
64
+ </section>
65
+ <section>
66
+ <h2>How much does it cost?</h2>
67
+ <p>
68
+ Generally less than a dollar per document set, but you're paying OpenAI, not me, and you're responsible for all costs, no matter what.
69
+ </p>
70
+ <p>
71
+ Eventually, some Meaningfully features may require payment.
72
+ </p>
73
+ </section>
74
+ <section>
75
+ <h2>How can I support development of Meaningfully??</h2>
76
+ <p>
77
+ You can <a href="https://buymeacoffee.com/jeremybmerrill">Buy Me A Coffee</a>.
78
+ </p>
79
+
80
+ </section>
81
+ </div>
82
+
83
+ <style>
84
+ .container {
85
+ max-width: 800px;
86
+ margin: 0 auto;
87
+ padding: 20px;
88
+ }
89
+
90
+ h1 {
91
+ font-size: 2rem;
92
+ margin-bottom: 1rem;
93
+ }
94
+
95
+ h2 {
96
+ font-size: 1.5rem;
97
+ margin-top: 1.5rem;
98
+ margin-bottom: 0.5rem;
99
+ }
100
+
101
+ p {
102
+ margin-bottom: 0.5rem;
103
+ margin-top: 0.5rem;
104
+ }
105
+
106
+ /* ol {
107
+ margin-left: 20px;
108
+ }
109
+ ol li {
110
+ list-style-type: decimal;
111
+ list-style-position: inside;
112
+ padding-left: 0.5rem;
113
+ } */
114
+ ul li {
115
+ list-style-type: disc;
116
+ list-style-position: inside;
117
+ padding-left: 0.5rem;
118
+ }
119
+ </style>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const HelpPage: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type HelpPage = InstanceType<typeof HelpPage>;
18
+ export default HelpPage;
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import Table from './Table.svelte';
3
+
4
+ interface Props {
5
+ previewData?: Array<Record<string, any>>;
6
+ textColumn: string;
7
+ metadataColumns?: string[];
8
+ loading?: boolean;
9
+ }
10
+
11
+ let {
12
+ previewData = [],
13
+ textColumn,
14
+ metadataColumns = [],
15
+ loading = false
16
+ }: Props = $props();
17
+ </script>
18
+
19
+ <div data-testid="preview" >
20
+ {#if loading}
21
+ <div class="flex justify-center items-center h-full">
22
+ <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-violet-500"></div>
23
+ </div>
24
+ { :else }
25
+ <div class="space-y-4">
26
+ <h2 class="text-xl font-semibold">Preview</h2>
27
+ <div class="bg-white rounded-lg shadow text-black">
28
+ <Table
29
+ data={previewData}
30
+ {textColumn}
31
+ {metadataColumns}
32
+ showSimilarity={false}
33
+ />
34
+ </div>
35
+ </div>
36
+ {/if}
37
+ </div>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ previewData?: Array<Record<string, any>>;
3
+ textColumn: string;
4
+ metadataColumns?: string[];
5
+ loading?: boolean;
6
+ }
7
+ declare const Preview: import("svelte").Component<Props, {}, "">;
8
+ type Preview = ReturnType<typeof Preview>;
9
+ export default Preview;
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ import Table from './Table.svelte';
3
+ import Papa from 'papaparse';
4
+
5
+ interface Props {
6
+ results?: Array<Record<string, any>>;
7
+ textColumn: string;
8
+ metadataColumns?: string[];
9
+ loading?: boolean;
10
+ originalDocumentClick?: (sourceNodeId: string) => void;
11
+ }
12
+
13
+ let {
14
+ results = [],
15
+ textColumn,
16
+ metadataColumns = [],
17
+ loading = false,
18
+ originalDocumentClick = () => {},
19
+ }: Props = $props();
20
+
21
+ // Initial number of results to display
22
+ const initialDisplayCount = 10;
23
+ let displayCount = $state(initialDisplayCount);
24
+
25
+ // Function to load more results
26
+ const showMore = () => {
27
+ displayCount += 10;
28
+ };
29
+
30
+ // Function to download results as CSV
31
+ const downloadCSV = () => {
32
+ if (results.length === 0) return;
33
+
34
+ // Prepare data for Papa Parse
35
+ const csvData = results.map(row => {
36
+ const csvRow: Record<string, any> = {};
37
+
38
+ // Add text column
39
+ csvRow[textColumn] = row[textColumn] || '';
40
+
41
+ // Add metadata columns
42
+ metadataColumns.forEach(column => {
43
+ csvRow[column] = row[column] || '';
44
+ });
45
+
46
+ // Add similarity column, formatted as percentage
47
+ if (row.similarity !== undefined) {
48
+ csvRow.similarity = (row.similarity * 100).toFixed(1) + '%';
49
+ }
50
+
51
+ return csvRow;
52
+ });
53
+
54
+ // Generate CSV using Papa Parse
55
+ const csvContent = Papa.unparse(csvData);
56
+
57
+ // Create blob and download
58
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
59
+ const url = URL.createObjectURL(blob);
60
+ const link = document.createElement('a');
61
+ link.href = url;
62
+ link.setAttribute('download', 'results.csv');
63
+ document.body.appendChild(link);
64
+ link.click();
65
+ document.body.removeChild(link);
66
+ URL.revokeObjectURL(url);
67
+ };
68
+
69
+ // Computed property for visible results
70
+ let visibleResults = $derived(results.slice(0, displayCount));
71
+ </script>
72
+
73
+ <div class="space-y-4">
74
+ <div class="flex justify-between items-center">
75
+ <h2 class="text-xl font-semibold">Search Results</h2>
76
+ {#if results.length > 0 && !loading}
77
+ <button
78
+ onclick={downloadCSV}
79
+ class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors flex items-center gap-2"
80
+ title="Download results as CSV"
81
+ >
82
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
83
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
84
+ </svg>
85
+ Download CSV
86
+ </button>
87
+ {/if}
88
+ </div>
89
+
90
+ {#if loading}
91
+ <div class="flex justify-center items-center h-full">
92
+ <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-violet-500"></div>
93
+ </div>
94
+ {:else if results.length === 0}
95
+ <div class="bg-white rounded-lg shadow text-black">
96
+ <p>No results found. Is it possible there is no data in the dataset?</p>
97
+ </div>
98
+ {:else}
99
+ <div class="bg-white rounded-lg shadow text-black">
100
+ <Table
101
+ data={visibleResults}
102
+ {textColumn}
103
+ {metadataColumns}
104
+ showSimilarity={true}
105
+ showShowOriginal={true}
106
+ originalDocumentClick={originalDocumentClick}
107
+ />
108
+ </div>
109
+
110
+ {#if displayCount < results.length}
111
+ <div class="flex justify-center mt-4">
112
+ <button
113
+ onclick={showMore}
114
+ class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
115
+ >
116
+ Show More
117
+ </button>
118
+ </div>
119
+ {/if}
120
+ {/if}
121
+ </div>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ results?: Array<Record<string, any>>;
3
+ textColumn: string;
4
+ metadataColumns?: string[];
5
+ loading?: boolean;
6
+ originalDocumentClick?: (sourceNodeId: string) => void;
7
+ }
8
+ declare const Results: import("svelte").Component<Props, {}, "">;
9
+ type Results = ReturnType<typeof Results>;
10
+ export default Results;
@@ -0,0 +1,256 @@
1
+ <script lang="ts">
2
+ import { navigate } from 'svelte-routing';
3
+ import type { DocumentSet, MeaningfullyAPI } from '../types';
4
+ import Results from './Results.svelte';
5
+
6
+ interface Props {
7
+ validApiKeysSet: boolean;
8
+ documentSetId: number;
9
+ api: MeaningfullyAPI;
10
+ }
11
+
12
+ let { validApiKeysSet, documentSetId, api }: Props = $props();
13
+
14
+ let documentSet: DocumentSet | null = $state(null);
15
+ let documentSetLoading = $state(true);
16
+ let metadataColumns: string[] = $state([]);
17
+ let textColumn: string = $state('');
18
+ let loading = $state(false);
19
+ let hasResults = $state(false);
20
+ let showModal = $state(false);
21
+ let modalContent: Record<string, any> | null = $state(null);
22
+
23
+ const blankSearchQuery = '';
24
+ let searchQuery = $state(blankSearchQuery);
25
+ let metadataFilters: Array<{ key: string, operator: "==" | "in" | ">" | "<" | "!=" | ">=" | "<=" | "nin" | "any" | "all" | "text_match" | "contains" | "is_empty", value: any }> = $state([]);
26
+
27
+ let results: Array<Record<string, any>> = $state([]);
28
+ let error: string | null = $state(null);
29
+
30
+ api.getDocumentSet(documentSetId).then((receivedDocumentSet: DocumentSet) => {
31
+ documentSet = receivedDocumentSet;
32
+ metadataColumns = (documentSet.parameters.metadataColumns ?? []) as string[];
33
+ // @ts-ignore
34
+ textColumn = documentSet.parameters.textColumns[0] as string;
35
+ documentSetLoading = false;
36
+ }).catch(error => {
37
+ console.error('Error fetching document set:', error);
38
+ navigate('/');
39
+ });
40
+
41
+ const placeholderQueries = [
42
+ "The CEO got fired",
43
+ "My car caught on fire as I was driving on the highway",
44
+ "I surprised my closest friends by starting a business selling handmade candles",
45
+ "Our company's stock price could plummet if we don't address the recent scandal involving our CEO",
46
+ "Don't tell anyone that I was the one who leaked the confidential information about our competitor's new product launch",
47
+ "I can't believe I got fired for accidentally sending a company-wide email with a meme instead of the quarterly report",
48
+ ];
49
+ const placeholderQuery = placeholderQueries[Math.floor(Math.random() * placeholderQueries.length)];
50
+
51
+ async function handleSearch() {
52
+ if (!searchQuery.trim() || !documentSet) return;
53
+ hasResults = true;
54
+ loading = true;
55
+ try {
56
+ const searchResults = await api.searchDocumentSet({
57
+ documentSetId: documentSet.documentSetId,
58
+ query: searchQuery,
59
+ n_results: 100,
60
+ filters: metadataFilters.map(filter => ({
61
+ key: filter.key,
62
+ operator: filter.operator,
63
+ value: filter.value
64
+ }))
65
+ });
66
+ results = searchResults.map(result => ({ // TODO Factor this out if preview and search use the same data structure.
67
+ ...result.metadata, // flatten the metadata so that this object is the same shape as a CSV row.
68
+ similarity: result.score.toFixed(2),
69
+ [textColumn]: result.text,
70
+ sourceNodeId: result.sourceNodeId
71
+ }));
72
+ error = null;
73
+ } catch (error_: any) {
74
+ console.error('Search failed:', error_);
75
+ error = error_;
76
+ } finally {
77
+ loading = false;
78
+ }
79
+ }
80
+
81
+ function addFilter() {
82
+ metadataFilters = [...metadataFilters, { key: '', operator: '==', value: '' }];
83
+ }
84
+
85
+ function removeFilter(index: number) {
86
+ metadataFilters = metadataFilters.filter((_, i) => i !== index);
87
+ }
88
+
89
+ async function handleOriginalDocumentClick( documentId: string) {
90
+ try {
91
+ const documentData = await api.getDocument({ documentSetId, documentId });
92
+ modalContent = documentData;
93
+ showModal = true;
94
+ } catch (error) {
95
+ console.error('Error fetching document:', error);
96
+ }
97
+ }
98
+
99
+ function closeModal() {
100
+ showModal = false;
101
+ modalContent = null;
102
+ }
103
+ </script>
104
+
105
+ <div class="p-6 space-y-6">
106
+ <div class="flex items-center space-x-4">
107
+ <button
108
+ class="text-blue-500 hover:text-blue-600 flex items-center space-x-1"
109
+ onclick={() => navigate('/') }
110
+ >
111
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
113
+ </svg>
114
+ <span>Back to Document Sets</span>
115
+ </button>
116
+ </div>
117
+
118
+ {#if documentSetLoading}
119
+ <p>Loading document set...</p>
120
+ {:else if !documentSet}
121
+ <p>Document set not found. {documentSetId}</p>
122
+ {:else}
123
+ <div class="space-y-2">
124
+ <h1 class="text-2xl font-bold" data-testid="document-set-name">{documentSet.name}</h1>
125
+ <p class="text-gray-600">
126
+ {documentSet.totalDocuments.toLocaleString()} documents • Uploaded {documentSet.uploadDate.toLocaleDateString()}
127
+ </p>
128
+ </div>
129
+
130
+ <div class="space-y-4 max-w-3xl">
131
+ <!-- Search Input -->
132
+ <div class="space-y-2">
133
+ <label for="search" class="block text-sm font-medium text-gray-700">
134
+ Semantic Search
135
+ </label>
136
+ <p class="text-xs text-gray-500">
137
+ Imagine the perfect document that you hope might exist in your spreadsheet. Type it here. Meaningfully will find the real documents that mean
138
+ about the same thing -- even if they have no keywords in common.
139
+ </p>
140
+ <div class="flex space-x-4">
141
+ <input
142
+ id="search"
143
+ type="text"
144
+ bind:value={searchQuery}
145
+ placeholder={placeholderQuery}
146
+ data-testid="search-bar"
147
+ class="flex-1 px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400"
148
+ />
149
+ <button
150
+ onclick={handleSearch}
151
+ disabled={loading || !validApiKeysSet}
152
+ data-testid="search-button"
153
+ class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
154
+ >
155
+ {loading ? 'Searching...' : 'Search'}
156
+ </button>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Metadata Filters -->
161
+ {#if metadataColumns.length > 0}
162
+ <div class="space-y-2">
163
+ <p class="block text-sm font-medium text-gray-700">
164
+ Search only records that match...
165
+ </p>
166
+ <div class="space-y-4">
167
+ {#each metadataFilters as filter, index}
168
+ <div class="flex space-x-2 items-center">
169
+ <select bind:value={filter.key} class="px-2 py-1 border border-gray-300 rounded-md">
170
+ <option value="" disabled>Select column</option>
171
+ {#each metadataColumns as column}
172
+ <option value={column}>{column}</option>
173
+ {/each}
174
+ </select>
175
+ <select bind:value={filter.operator} class="px-2 py-1 border border-gray-300 rounded-md">
176
+ <option value="==">==</option>
177
+ <option value="in">in</option>
178
+ <option value=">">&gt;</option>
179
+ <option value="<">&lt;</option>
180
+ <option value="!=">!=</option>
181
+ <option value=">=">&gt;=</option>
182
+ <option value="<=">&lt;=</option>
183
+ <option value="nin">not in</option>
184
+ <option value="any">any</option>
185
+ <option value="all">all</option>
186
+ <option value="text_match">text matches</option>
187
+ <option value="contains">contains</option>
188
+ <option value="is_empty">is empty</option>
189
+ </select>
190
+ <input
191
+ type="text"
192
+ bind:value={filter.value}
193
+ placeholder="Value"
194
+ class="flex-1 px-2 py-1 border border-gray-300 rounded-md"
195
+ />
196
+ <button onclick={() => removeFilter(index)} class="text-red-500 hover:text-red-600">
197
+ Remove
198
+ </button>
199
+ </div>
200
+ {/each}
201
+ <button onclick={addFilter} class="text-blue-500 hover:text-blue-600">
202
+ Add Filter
203
+ </button>
204
+ </div>
205
+ </div>
206
+ {/if}
207
+ </div>
208
+
209
+ <!-- Results -->
210
+ {#if error}
211
+ <div class="my-10 p-4 bg-red-100 text-red-700 rounded-md">
212
+ {error}
213
+ </div>
214
+ {/if}
215
+ {#if (searchQuery != blankSearchQuery || metadataFilters.length > 0) && hasResults}
216
+ <!-- Wrap Results component for easier selection -->
217
+ <div data-testid="results">
218
+ <Results
219
+ {results}
220
+ {loading}
221
+ {textColumn}
222
+ {metadataColumns}
223
+ originalDocumentClick={handleOriginalDocumentClick}
224
+ />
225
+ </div>
226
+ {/if}
227
+ {/if}
228
+ </div>
229
+
230
+ <!-- modal for showing a whole document -->
231
+ {#if showModal && modalContent}
232
+ <div data-testid="details" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
233
+ <div class="bg-white text-black p-6 rounded-lg shadow-lg max-w-xl w-full max-h-screen overflow-y-auto">
234
+ <h2 class="text-xl font-semibold mb-4">Original Document</h2>
235
+ <table>
236
+ <thead>
237
+ <tr>
238
+ </tr>
239
+ </thead>
240
+ <tbody>
241
+ <tr>
242
+ <td class="px-4 py-2 text-left border-b text-black">Original text</td>
243
+ <td class="px-4 py-2 border-b text-black">{modalContent.text}</td>
244
+ </tr>
245
+ {#each metadataColumns as key}
246
+ <tr>
247
+ <td class="px-4 py-2 text-left border-b text-black">{key}</td>
248
+ <td class="px-4 py-2 border-b text-black">{modalContent.metadata[key]}</td>
249
+ </tr>
250
+ {/each}
251
+ </tbody>
252
+ </table>
253
+ <button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onclick={closeModal}>Close</button>
254
+ </div>
255
+ </div>
256
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { MeaningfullyAPI } from '../types';
2
+ interface Props {
3
+ validApiKeysSet: boolean;
4
+ documentSetId: number;
5
+ api: MeaningfullyAPI;
6
+ }
7
+ declare const SearchPage: import("svelte").Component<Props, {}, "">;
8
+ type SearchPage = ReturnType<typeof SearchPage>;
9
+ export default SearchPage;