@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.
- package/LICENSE +7 -0
- package/README.md +3 -0
- package/dist/App.svelte +86 -0
- package/dist/App.svelte.d.ts +7 -0
- package/dist/assets/base.css +56 -0
- package/dist/assets/electron.svg +10 -0
- package/dist/assets/main.css +171 -0
- package/dist/components/ApiKeyPage.svelte +128 -0
- package/dist/components/ApiKeyPage.svelte.d.ts +9 -0
- package/dist/components/ApiKeyStatus.svelte +38 -0
- package/dist/components/ApiKeyStatus.svelte.d.ts +7 -0
- package/dist/components/CsvUpload.svelte +101 -0
- package/dist/components/CsvUpload.svelte.d.ts +5 -0
- package/dist/components/DatabaseConfig.svelte +485 -0
- package/dist/components/DatabaseConfig.svelte.d.ts +6 -0
- package/dist/components/ExistingDatabases.svelte +175 -0
- package/dist/components/ExistingDatabases.svelte.d.ts +11 -0
- package/dist/components/FrontPage.svelte +18 -0
- package/dist/components/FrontPage.svelte.d.ts +8 -0
- package/dist/components/HelpPage.svelte +119 -0
- package/dist/components/HelpPage.svelte.d.ts +18 -0
- package/dist/components/Preview.svelte +37 -0
- package/dist/components/Preview.svelte.d.ts +9 -0
- package/dist/components/Results.svelte +121 -0
- package/dist/components/Results.svelte.d.ts +10 -0
- package/dist/components/SearchPage.svelte +256 -0
- package/dist/components/SearchPage.svelte.d.ts +9 -0
- package/dist/components/Table.svelte +87 -0
- package/dist/components/Table.svelte.d.ts +11 -0
- package/dist/env.d.ts +39 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/stores/fileDataStore.d.ts +8 -0
- package/dist/stores/fileDataStore.js +3 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +1 -0
- 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=">">></option>
|
|
179
|
+
<option value="<"><</option>
|
|
180
|
+
<option value="!=">!=</option>
|
|
181
|
+
<option value=">=">>=</option>
|
|
182
|
+
<option value="<="><=</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;
|