@meaningfully/ui 0.0.9 → 0.0.11
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/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
# meaningfully-ui
|
|
2
2
|
|
|
3
|
-
This is a library of UI components for [meaningfully](https://www.github.com/jeremybmerrill/meaningfully), which is an app (or collection of apps!) for semantic search over spreadsheets. See the main repo URL for more discussion.
|
|
3
|
+
This is a library of UI components for [meaningfully](https://www.github.com/jeremybmerrill/meaningfully), which is an app (or collection of apps!) for semantic search over spreadsheets. See the main repo URL for more discussion.
|
|
4
|
+
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
How to run:
|
|
8
|
+
|
|
9
|
+
- `npm run build` to build
|
|
10
|
+
- `npm run watch` to build continuously, rebuilding on file modification.
|
|
@@ -29,14 +29,9 @@
|
|
|
29
29
|
let chunkSize = $state(defaultChunkSize);
|
|
30
30
|
let chunkOverlap = $state(defaultChunkOverlap);
|
|
31
31
|
|
|
32
|
-
// Model options grouped by provider
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"azure": ["text-embedding-3-small", "text-embedding-3-large"],
|
|
36
|
-
"ollama": ["mxbai-embed-large", "nomic-embed-text"],
|
|
37
|
-
"mistral": ["mistral-embed"],
|
|
38
|
-
"gemini": ["gemini-embedding-001"]
|
|
39
|
-
};
|
|
32
|
+
// Model options grouped by provider - fetched from API
|
|
33
|
+
let availableModelOptions: Record<string, string[]> = $state({});
|
|
34
|
+
let allModelOptions: Record<string, string[]> = $state({});
|
|
40
35
|
|
|
41
36
|
let modelProvider = $state("openai");
|
|
42
37
|
let modelName = $state("text-embedding-3-small");
|
|
@@ -50,8 +45,8 @@
|
|
|
50
45
|
|
|
51
46
|
// Update model name when provider changes
|
|
52
47
|
$effect(() => {
|
|
53
|
-
if (modelProvider &&
|
|
54
|
-
modelName =
|
|
48
|
+
if (modelProvider && availableModelOptions[modelProvider]) {
|
|
49
|
+
modelName = availableModelOptions[modelProvider][0];
|
|
55
50
|
}
|
|
56
51
|
});
|
|
57
52
|
|
|
@@ -78,7 +73,23 @@
|
|
|
78
73
|
return result;
|
|
79
74
|
};
|
|
80
75
|
|
|
81
|
-
onMount(() => {
|
|
76
|
+
onMount(async () => {
|
|
77
|
+
// Fetch available model options from API
|
|
78
|
+
try {
|
|
79
|
+
const modelOptionsData = await api.getAvailableModelOptions();
|
|
80
|
+
availableModelOptions = modelOptionsData.availableModelOptions;
|
|
81
|
+
allModelOptions = modelOptionsData.allModelOptions;
|
|
82
|
+
|
|
83
|
+
// Set default provider to first available one
|
|
84
|
+
const availableProviders = Object.keys(availableModelOptions);
|
|
85
|
+
if (availableProviders.length > 0) {
|
|
86
|
+
modelProvider = availableProviders[0];
|
|
87
|
+
modelName = availableModelOptions[modelProvider][0];
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('Error fetching available model options:', e);
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
// Subscribe to the file data store
|
|
83
94
|
const unsubscribe = fileDataStore.subscribe((data: any) => {
|
|
84
95
|
if (!data) {
|
|
@@ -284,15 +295,15 @@
|
|
|
284
295
|
<select
|
|
285
296
|
bind:value={modelProvider}
|
|
286
297
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-violet-500 focus:ring-violet-500">
|
|
287
|
-
<option value="openai">OpenAI</option>
|
|
288
|
-
<option value="azure">Azure OpenAI</option>
|
|
289
|
-
<option value="ollama">Ollama</option>
|
|
290
|
-
<option value="mistral">Mistral AI</option>
|
|
291
|
-
<option value="gemini">Google Gemini</option>
|
|
298
|
+
<option value="openai" disabled={!availableModelOptions.openai}>OpenAI</option>
|
|
299
|
+
<option value="azure" disabled={!availableModelOptions.azure}>Azure OpenAI</option>
|
|
300
|
+
<option value="ollama" disabled={!availableModelOptions.ollama}>Ollama</option>
|
|
301
|
+
<option value="mistral" disabled={!availableModelOptions.mistral}>Mistral AI</option>
|
|
302
|
+
<option value="gemini" disabled={!availableModelOptions.gemini}>Google Gemini</option>
|
|
292
303
|
</select>
|
|
293
304
|
</label>
|
|
294
305
|
<p class="text-xs text-gray-500">
|
|
295
|
-
The provider for the embedding model.
|
|
306
|
+
The provider for the embedding model. {Object.keys(availableModelOptions).length === 0 ? 'No providers are configured yet. Please set up API keys in the settings.' : ''}
|
|
296
307
|
</p>
|
|
297
308
|
</div>
|
|
298
309
|
|
|
@@ -301,8 +312,9 @@
|
|
|
301
312
|
What embedding model should we use?
|
|
302
313
|
<select
|
|
303
314
|
bind:value={modelName}
|
|
304
|
-
|
|
305
|
-
|
|
315
|
+
disabled={!availableModelOptions[modelProvider]}
|
|
316
|
+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-violet-500 focus:ring-violet-500 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
317
|
+
{#each (availableModelOptions[modelProvider] || allModelOptions[modelProvider] || []) as modelNameChoice}
|
|
306
318
|
<option value={modelNameChoice}>{modelNameChoice}</option>
|
|
307
319
|
{/each}
|
|
308
320
|
</select>
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
textColumn: string;
|
|
8
8
|
metadataColumns?: string[];
|
|
9
9
|
loading?: boolean;
|
|
10
|
+
hasMore?: boolean;
|
|
11
|
+
loadingMore?: boolean;
|
|
12
|
+
showMore?: () => void;
|
|
10
13
|
originalDocumentClick?: (sourceNodeId: string) => void;
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -15,18 +18,12 @@
|
|
|
15
18
|
textColumn,
|
|
16
19
|
metadataColumns = [],
|
|
17
20
|
loading = false,
|
|
21
|
+
hasMore = false,
|
|
22
|
+
loadingMore = false,
|
|
23
|
+
showMore = () => {},
|
|
18
24
|
originalDocumentClick = () => {},
|
|
19
25
|
}: Props = $props();
|
|
20
26
|
|
|
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
27
|
// copied from https://github.com/run-llama/LlamaIndexTS/blob/main/packages/providers/storage/weaviate/src/sanitize.ts
|
|
31
28
|
// weaviate requires property names (i.e. metadata column names) to start with a lowercase letter or underscore,
|
|
32
29
|
// and only contain letters, numbers, and underscores.
|
|
@@ -95,12 +92,12 @@
|
|
|
95
92
|
link.setAttribute('download', 'results.csv');
|
|
96
93
|
document.body.appendChild(link);
|
|
97
94
|
link.click();
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
document.body.removeChild(link);
|
|
97
|
+
URL.revokeObjectURL(url);
|
|
98
|
+
}, 5000);
|
|
100
99
|
};
|
|
101
100
|
|
|
102
|
-
// Computed property for visible results
|
|
103
|
-
let visibleResults = $derived(results.slice(0, displayCount));
|
|
104
101
|
</script>
|
|
105
102
|
|
|
106
103
|
<div class="space-y-4">
|
|
@@ -111,6 +108,7 @@
|
|
|
111
108
|
onclick={downloadCSV}
|
|
112
109
|
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors flex items-center gap-2"
|
|
113
110
|
title="Download results as CSV"
|
|
111
|
+
data-testid="download-csv"
|
|
114
112
|
>
|
|
115
113
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
116
114
|
<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>
|
|
@@ -131,7 +129,7 @@
|
|
|
131
129
|
{:else}
|
|
132
130
|
<div class="bg-white rounded-lg shadow text-black">
|
|
133
131
|
<Table
|
|
134
|
-
data={
|
|
132
|
+
data={results}
|
|
135
133
|
{textColumn}
|
|
136
134
|
{metadataColumns}
|
|
137
135
|
showSimilarity={true}
|
|
@@ -140,13 +138,14 @@
|
|
|
140
138
|
/>
|
|
141
139
|
</div>
|
|
142
140
|
|
|
143
|
-
{#if
|
|
141
|
+
{#if hasMore}
|
|
144
142
|
<div class="flex justify-center mt-4">
|
|
145
143
|
<button
|
|
146
144
|
onclick={showMore}
|
|
145
|
+
disabled={loadingMore}
|
|
147
146
|
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
|
148
147
|
>
|
|
149
|
-
Show More
|
|
148
|
+
{loadingMore ? 'Loading...' : 'Show More'}
|
|
150
149
|
</button>
|
|
151
150
|
</div>
|
|
152
151
|
{/if}
|
|
@@ -3,6 +3,9 @@ interface Props {
|
|
|
3
3
|
textColumn: string;
|
|
4
4
|
metadataColumns?: string[];
|
|
5
5
|
loading?: boolean;
|
|
6
|
+
hasMore?: boolean;
|
|
7
|
+
loadingMore?: boolean;
|
|
8
|
+
showMore?: () => void;
|
|
6
9
|
originalDocumentClick?: (sourceNodeId: string) => void;
|
|
7
10
|
}
|
|
8
11
|
declare const Results: import("svelte").Component<Props, {}, "">;
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
let metadataColumns: string[] = $state([]);
|
|
18
18
|
let textColumn: string = $state('');
|
|
19
19
|
let loading = $state(false);
|
|
20
|
+
let loadingMore = $state(false);
|
|
20
21
|
let hasResults = $state(false);
|
|
22
|
+
let hasMore = $state(false);
|
|
21
23
|
let showModal = $state(false);
|
|
22
24
|
let modalContent: Record<string, any> | null = $state(null);
|
|
23
25
|
|
|
@@ -28,6 +30,8 @@
|
|
|
28
30
|
let results: Array<Record<string, any>> = $state([]);
|
|
29
31
|
let error: string | null = $state(null);
|
|
30
32
|
|
|
33
|
+
const pageSize = 10;
|
|
34
|
+
|
|
31
35
|
api.getDocumentSet(documentSetId).then((receivedDocumentSet: DocumentSet) => {
|
|
32
36
|
documentSet = receivedDocumentSet;
|
|
33
37
|
documentSet.uploadDate = new Date(documentSet.uploadDate); // convert to Date object, if necessary (SQLite returns date obj, Postgres returns string)
|
|
@@ -50,36 +54,74 @@
|
|
|
50
54
|
];
|
|
51
55
|
const placeholderQuery = placeholderQueries[Math.floor(Math.random() * placeholderQueries.length)];
|
|
52
56
|
|
|
57
|
+
const mapSearchResults = (searchResults: Array<Record<string, any>>) => {
|
|
58
|
+
return searchResults.map(result => ({ // TODO Factor this out if preview and search use the same data structure.
|
|
59
|
+
...result.metadata, // flatten the metadata so that this object is the same shape as a CSV row.
|
|
60
|
+
similarity: result.score,
|
|
61
|
+
[textColumn]: result.text,
|
|
62
|
+
sourceNodeId: result.sourceNodeId
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
|
|
53
66
|
async function handleSearch() {
|
|
54
67
|
if (!searchQuery.trim() || !documentSet) return;
|
|
55
68
|
hasResults = true;
|
|
56
69
|
loading = true;
|
|
70
|
+
hasMore = false;
|
|
57
71
|
try {
|
|
58
|
-
const
|
|
72
|
+
const searchResponse = await api.searchDocumentSet({
|
|
59
73
|
documentSetId: documentSet.documentSetId,
|
|
60
74
|
query: searchQuery,
|
|
61
|
-
n_results:
|
|
75
|
+
n_results: pageSize,
|
|
76
|
+
offset: 0,
|
|
62
77
|
filters: metadataFilters.map(filter => ({
|
|
63
78
|
key: filter.key,
|
|
64
79
|
operator: filter.operator,
|
|
65
80
|
value: filter.value
|
|
66
81
|
}))
|
|
67
82
|
});
|
|
68
|
-
results =
|
|
69
|
-
|
|
70
|
-
similarity: result.score.toFixed(2),
|
|
71
|
-
[textColumn]: result.text,
|
|
72
|
-
sourceNodeId: result.sourceNodeId
|
|
73
|
-
}));
|
|
83
|
+
results = mapSearchResults(searchResponse.results);
|
|
84
|
+
hasMore = searchResponse.hasMore;
|
|
74
85
|
error = null;
|
|
75
86
|
} catch (error_: any) {
|
|
76
87
|
console.error('Search failed:', error_);
|
|
77
|
-
error = error_;
|
|
88
|
+
error = error_ instanceof Error ? error_.message : String(error_);
|
|
89
|
+
results = [];
|
|
90
|
+
hasMore = false;
|
|
78
91
|
} finally {
|
|
79
92
|
loading = false;
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
async function handleLoadMore() {
|
|
97
|
+
if (!documentSet || loadingMore || loading || !hasMore) return;
|
|
98
|
+
|
|
99
|
+
loadingMore = true;
|
|
100
|
+
try {
|
|
101
|
+
const searchResponse = await api.searchDocumentSet({
|
|
102
|
+
documentSetId: documentSet.documentSetId,
|
|
103
|
+
query: searchQuery,
|
|
104
|
+
n_results: pageSize,
|
|
105
|
+
offset: results.length,
|
|
106
|
+
filters: metadataFilters.map(filter => ({
|
|
107
|
+
key: filter.key,
|
|
108
|
+
operator: filter.operator,
|
|
109
|
+
value: filter.value
|
|
110
|
+
}))
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const nextRows = mapSearchResults(searchResponse.results);
|
|
114
|
+
results = [...results, ...nextRows];
|
|
115
|
+
hasMore = searchResponse.hasMore;
|
|
116
|
+
error = null;
|
|
117
|
+
} catch (error_: any) {
|
|
118
|
+
console.error('Loading more failed:', error_);
|
|
119
|
+
error = error_ instanceof Error ? error_.message : String(error_);
|
|
120
|
+
} finally {
|
|
121
|
+
loadingMore = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
function addFilter() {
|
|
84
126
|
metadataFilters = [...metadataFilters, { key: '', operator: '==', value: '' }];
|
|
85
127
|
}
|
|
@@ -220,6 +262,9 @@
|
|
|
220
262
|
<Results
|
|
221
263
|
{results}
|
|
222
264
|
{loading}
|
|
265
|
+
{loadingMore}
|
|
266
|
+
{hasMore}
|
|
267
|
+
showMore={handleLoadMore}
|
|
223
268
|
{textColumn}
|
|
224
269
|
{metadataColumns}
|
|
225
270
|
originalDocumentClick={handleOriginalDocumentClick}
|
|
@@ -254,7 +299,7 @@
|
|
|
254
299
|
{/each}
|
|
255
300
|
</tbody>
|
|
256
301
|
</table>
|
|
257
|
-
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onclick={closeModal}>Close</button>
|
|
302
|
+
<button data-testid="modal-close-button" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" onclick={closeModal}>Close</button>
|
|
258
303
|
</div>
|
|
259
304
|
</div>
|
|
260
305
|
{/if}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,12 +32,16 @@ export interface MeaningfullyAPI {
|
|
|
32
32
|
documentSetId: number;
|
|
33
33
|
query: string;
|
|
34
34
|
n_results: number;
|
|
35
|
+
offset?: number;
|
|
35
36
|
filters?: {
|
|
36
37
|
key: string;
|
|
37
38
|
operator: "==" | "in" | ">" | "<" | "!=" | ">=" | "<=" | "nin" | "any" | "all" | "text_match" | "contains" | "is_empty";
|
|
38
39
|
value: any;
|
|
39
40
|
}[];
|
|
40
|
-
}) => Promise<
|
|
41
|
+
}) => Promise<{
|
|
42
|
+
results: SearchResult[];
|
|
43
|
+
hasMore: boolean;
|
|
44
|
+
}>;
|
|
41
45
|
getDocument: (params: {
|
|
42
46
|
documentSetId: number;
|
|
43
47
|
documentId: string;
|
|
@@ -57,4 +61,8 @@ export interface MeaningfullyAPI {
|
|
|
57
61
|
elapsedTimeMs: number;
|
|
58
62
|
estimatedTimeRemainingMs: number | null;
|
|
59
63
|
}>;
|
|
64
|
+
getAvailableModelOptions: () => Promise<{
|
|
65
|
+
availableModelOptions: Record<string, string[]>;
|
|
66
|
+
allModelOptions: Record<string, string[]>;
|
|
67
|
+
}>;
|
|
60
68
|
}
|