@reddb-io/ui 0.1.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 +17 -0
- package/bin/red-ui.js +259 -0
- package/build/CNAME +1 -0
- package/build/_app/env.js +1 -0
- package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
- package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
- package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
- package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
- package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
- package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
- package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
- package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
- package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
- package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
- package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
- package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
- package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
- package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
- package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
- package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
- package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
- package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
- package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
- package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
- package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
- package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
- package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
- package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
- package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
- package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
- package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
- package/build/_app/version.json +1 -0
- package/build/favicon.png +0 -0
- package/build/index.html +59 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
- package/dist/embed/core-QuRYvMt8.js +118 -0
- package/dist/embed/core-QuRYvMt8.js.map +1 -0
- package/dist/embed/index-B6ZQhCsu.js +577 -0
- package/dist/embed/index-B6ZQhCsu.js.map +1 -0
- package/dist/embed/index-client-ItCEU64C.js +3087 -0
- package/dist/embed/index-client-ItCEU64C.js.map +1 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
- package/dist/embed/index.js +11 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
- package/dist/embed/ui.css +1 -0
- package/package.json +101 -0
- package/src/app.css +238 -0
- package/src/lib/ActivityIndicator.svelte +100 -0
- package/src/lib/ClusterNode.svelte +119 -0
- package/src/lib/ClusterView.svelte +541 -0
- package/src/lib/CollectionHistory.svelte +266 -0
- package/src/lib/CollectionsView.svelte +14 -0
- package/src/lib/CommandPalette.svelte +237 -0
- package/src/lib/ConnectDropdown.svelte +222 -0
- package/src/lib/EmptyState.svelte +27 -0
- package/src/lib/LiveChanges.svelte +195 -0
- package/src/lib/MasterPasswordDialog.svelte +168 -0
- package/src/lib/PageHeader.svelte +32 -0
- package/src/lib/PendingChangesPanel.svelte +153 -0
- package/src/lib/QueryEditor.svelte +122 -0
- package/src/lib/ResultsPane.svelte +747 -0
- package/src/lib/SchemaTree.svelte +193 -0
- package/src/lib/SecurityView.svelte +255 -0
- package/src/lib/ShortcutOverlay.svelte +104 -0
- package/src/lib/Splash.svelte +46 -0
- package/src/lib/StatusBar.svelte +163 -0
- package/src/lib/SubNav.svelte +62 -0
- package/src/lib/Topbar.svelte +119 -0
- package/src/lib/Workspace.svelte +247 -0
- package/src/lib/activity.svelte.ts +91 -0
- package/src/lib/build-info.ts +41 -0
- package/src/lib/capability.ts +221 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/collection-catalog.ts +116 -0
- package/src/lib/collection-pages.ts +33 -0
- package/src/lib/connection-history.ts +301 -0
- package/src/lib/connections.svelte.ts +493 -0
- package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
- package/src/lib/embed/index.ts +234 -0
- package/src/lib/graph-loader/contract.ts +336 -0
- package/src/lib/graph-loader/drilldown.ts +334 -0
- package/src/lib/graph-loader/index.ts +13 -0
- package/src/lib/index.ts +114 -0
- package/src/lib/pending-changes.svelte.ts +149 -0
- package/src/lib/query-tabs.svelte.ts +130 -0
- package/src/lib/reddb/boot-params.ts +254 -0
- package/src/lib/reddb/cdc-stream-client.ts +234 -0
- package/src/lib/reddb/client.ts +698 -0
- package/src/lib/reddb/connection-bootstrap.ts +260 -0
- package/src/lib/reddb/connection-provider.contract.ts +72 -0
- package/src/lib/reddb/connection-provider.ts +72 -0
- package/src/lib/reddb/index.ts +14 -0
- package/src/lib/reddb/injected-client-provider.ts +76 -0
- package/src/lib/reddb/local-url-provider.ts +245 -0
- package/src/lib/reddb/secure-store.ts +41 -0
- package/src/lib/reddb/server-capabilities.ts +71 -0
- package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
- package/src/lib/reddb/transports.ts +89 -0
- package/src/lib/reddb/types.ts +32 -0
- package/src/lib/reddb/uri.ts +54 -0
- package/src/lib/reddb/web-encrypted-store.ts +201 -0
- package/src/lib/renderers/DiffRenderer.svelte +184 -0
- package/src/lib/renderers/DocumentRenderer.svelte +134 -0
- package/src/lib/renderers/GraphRenderer.svelte +1328 -0
- package/src/lib/renderers/HypertableRenderer.svelte +253 -0
- package/src/lib/renderers/JsonRenderer.svelte +30 -0
- package/src/lib/renderers/KvRenderer.svelte +150 -0
- package/src/lib/renderers/QueueRenderer.svelte +286 -0
- package/src/lib/renderers/StatsRenderer.svelte +123 -0
- package/src/lib/renderers/TableRenderer.svelte +440 -0
- package/src/lib/renderers/VectorRenderer.svelte +388 -0
- package/src/lib/renderers/diff-render.ts +122 -0
- package/src/lib/renderers/diff.ts +9 -0
- package/src/lib/renderers/document-render.ts +64 -0
- package/src/lib/renderers/document.ts +9 -0
- package/src/lib/renderers/graph-render.ts +631 -0
- package/src/lib/renderers/graph-sigma.ts +395 -0
- package/src/lib/renderers/graph.ts +9 -0
- package/src/lib/renderers/hypertable-render.ts +320 -0
- package/src/lib/renderers/hypertable.ts +9 -0
- package/src/lib/renderers/index.ts +42 -0
- package/src/lib/renderers/json-render.ts +89 -0
- package/src/lib/renderers/json.ts +10 -0
- package/src/lib/renderers/kv-render.ts +193 -0
- package/src/lib/renderers/kv.ts +9 -0
- package/src/lib/renderers/queue-render.ts +197 -0
- package/src/lib/renderers/queue.ts +9 -0
- package/src/lib/renderers/registry.ts +68 -0
- package/src/lib/renderers/stats-render.ts +73 -0
- package/src/lib/renderers/stats.ts +9 -0
- package/src/lib/renderers/table-render.ts +156 -0
- package/src/lib/renderers/table.ts +11 -0
- package/src/lib/renderers/types.ts +33 -0
- package/src/lib/renderers/vector-render.ts +154 -0
- package/src/lib/renderers/vector.ts +9 -0
- package/src/lib/router.svelte.ts +163 -0
- package/src/lib/secureStore.svelte.ts +148 -0
- package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
- package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
- package/src/lib/shortcuts.ts +22 -0
- package/src/lib/surface.ts +80 -0
- package/src/lib/tabs.svelte.ts +115 -0
- package/src/lib/theme.svelte.ts +63 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { QueryResult, RedClient } from '#reddb'
|
|
3
|
+
import { Braces, Gauge, ListFilter, LoaderCircle, Play, Rows3, Search, X } from 'lucide-svelte'
|
|
4
|
+
import {
|
|
5
|
+
buildVectorSearchQuery,
|
|
6
|
+
compactValue,
|
|
7
|
+
extractVectors,
|
|
8
|
+
isSyntheticVectorPreview,
|
|
9
|
+
isVectorSearchResult,
|
|
10
|
+
parseVectorInput,
|
|
11
|
+
scalarLabel,
|
|
12
|
+
vectorMagnitude,
|
|
13
|
+
type VectorRow,
|
|
14
|
+
} from './vector-render'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
result: QueryResult
|
|
18
|
+
collection?: string
|
|
19
|
+
client?: RedClient
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { result, collection, client }: Props = $props()
|
|
23
|
+
|
|
24
|
+
let surface = $state<'browse' | 'search'>('browse')
|
|
25
|
+
let mode = $state<'metadata' | 'vector' | 'scalar'>('metadata')
|
|
26
|
+
let source = $state<'text' | 'vector'>('text')
|
|
27
|
+
let queryText = $state('')
|
|
28
|
+
let queryVector = $state('')
|
|
29
|
+
let limit = $state(20)
|
|
30
|
+
let filter = $state('')
|
|
31
|
+
let selected = $state<VectorRow | null>(null)
|
|
32
|
+
let searchResult = $state<QueryResult | null>(null)
|
|
33
|
+
let searching = $state(false)
|
|
34
|
+
let searchError = $state<string | null>(null)
|
|
35
|
+
|
|
36
|
+
const browseRows = $derived(extractVectors(result))
|
|
37
|
+
const declaredDimension = $derived(browseRows.find((row) => row.dimension > 0)?.dimension ?? null)
|
|
38
|
+
const incomingSearchResult = $derived(searchResult ?? (isVectorSearchResult(result) ? result : null))
|
|
39
|
+
const searchRows = $derived(incomingSearchResult ? extractVectors(incomingSearchResult) : [])
|
|
40
|
+
const rows = $derived(surface === 'search' ? searchRows : browseRows)
|
|
41
|
+
const syntheticPreview = $derived(incomingSearchResult ? isSyntheticVectorPreview(incomingSearchResult) : false)
|
|
42
|
+
const filtered = $derived.by(() => {
|
|
43
|
+
const q = filter.trim().toLowerCase()
|
|
44
|
+
if (!q) return rows
|
|
45
|
+
return rows.filter((row) =>
|
|
46
|
+
row.id.toLowerCase().includes(q) ||
|
|
47
|
+
Object.values(row.metadata).some((v) => compactValue(v).toLowerCase().includes(q)),
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
const active = $derived(selected ?? filtered[0] ?? null)
|
|
51
|
+
const maxMagnitude = $derived(Math.max(1, ...filtered.map((row) => vectorMagnitude(row.vector))))
|
|
52
|
+
const hasSearch = $derived(incomingSearchResult !== null)
|
|
53
|
+
|
|
54
|
+
function preview(row: VectorRow): string {
|
|
55
|
+
const entries = Object.entries(row.metadata).slice(0, 3)
|
|
56
|
+
if (entries.length === 0) return ''
|
|
57
|
+
return entries.map(([k, v]) => `${k}=${compactValue(v)}`).join(' · ')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function barWidth(value: number, vector: number[]): number {
|
|
61
|
+
const max = Math.max(1, ...vector.map((x) => Math.abs(x)))
|
|
62
|
+
return Math.max(2, Math.abs(value) / max * 100)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runSearch() {
|
|
66
|
+
if (!client || !collection) {
|
|
67
|
+
searchError = 'No active RedDB connection for vector search.'
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
searchError = null
|
|
71
|
+
searching = true
|
|
72
|
+
try {
|
|
73
|
+
const text = queryText.trim()
|
|
74
|
+
if (source === 'text' && text.length === 0) {
|
|
75
|
+
throw new Error('Enter text for RedDB to embed.')
|
|
76
|
+
}
|
|
77
|
+
const query = source === 'text'
|
|
78
|
+
? buildVectorSearchQuery(collection, { kind: 'text', text }, limit)
|
|
79
|
+
: buildVectorSearchQuery(collection, { kind: 'vector', vector: parseVectorInput(queryVector) }, limit)
|
|
80
|
+
const next = await client.query(query)
|
|
81
|
+
searchResult = next
|
|
82
|
+
selected = null
|
|
83
|
+
surface = 'search'
|
|
84
|
+
mode = 'metadata'
|
|
85
|
+
} catch (e) {
|
|
86
|
+
searchError = (e as Error).message
|
|
87
|
+
} finally {
|
|
88
|
+
searching = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<div class="flex h-full flex-col text-fg-1">
|
|
94
|
+
<div class="border-b border-line-1 bg-bg-1/40 px-3 py-2 font-mono text-[11px]">
|
|
95
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
96
|
+
<div class="inline-flex shrink-0 overflow-hidden rounded border border-line-2">
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class={['inline-flex h-7 items-center gap-1 px-2 transition-colors', surface === 'browse' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
100
|
+
onclick={() => { surface = 'browse'; selected = null }}
|
|
101
|
+
aria-pressed={surface === 'browse'}
|
|
102
|
+
>
|
|
103
|
+
<Rows3 class="size-3" />
|
|
104
|
+
browse
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
class={['inline-flex h-7 items-center gap-1 border-l border-line-2 px-2 transition-colors', surface === 'search' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
109
|
+
onclick={() => { surface = 'search'; selected = null }}
|
|
110
|
+
aria-pressed={surface === 'search'}
|
|
111
|
+
>
|
|
112
|
+
<Search class="size-3" />
|
|
113
|
+
search
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{#if surface === 'search'}
|
|
118
|
+
<div class="inline-flex shrink-0 overflow-hidden rounded border border-line-2">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class={['inline-flex h-7 items-center gap-1 px-2 transition-colors', source === 'text' ? 'bg-bg-3 text-fg-0' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
122
|
+
onclick={() => (source = 'text')}
|
|
123
|
+
aria-pressed={source === 'text'}
|
|
124
|
+
>
|
|
125
|
+
text
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
class={['inline-flex h-7 items-center gap-1 border-l border-line-2 px-2 transition-colors', source === 'vector' ? 'bg-bg-3 text-fg-0' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
130
|
+
onclick={() => (source = 'vector')}
|
|
131
|
+
aria-pressed={source === 'vector'}
|
|
132
|
+
>
|
|
133
|
+
vector
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="min-w-[260px] flex-1">
|
|
138
|
+
{#if source === 'text'}
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
bind:value={queryText}
|
|
142
|
+
placeholder="text query…"
|
|
143
|
+
class="h-7 w-full rounded border border-line-1 bg-bg-0 px-2 text-fg-1 outline-none placeholder:text-fg-3 focus:border-line-3"
|
|
144
|
+
onkeydown={(e) => { if (e.key === 'Enter') void runSearch() }}
|
|
145
|
+
/>
|
|
146
|
+
{:else}
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
bind:value={queryVector}
|
|
150
|
+
placeholder="[0.12, -0.04, 0.91, …]"
|
|
151
|
+
class="h-7 w-full rounded border border-line-1 bg-bg-0 px-2 text-fg-1 outline-none placeholder:text-fg-3 focus:border-line-3"
|
|
152
|
+
onkeydown={(e) => { if (e.key === 'Enter') void runSearch() }}
|
|
153
|
+
/>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<label class="inline-flex h-7 items-center gap-1 rounded border border-line-1 bg-bg-0 px-2 text-fg-3">
|
|
158
|
+
<span>limit</span>
|
|
159
|
+
<input
|
|
160
|
+
type="number"
|
|
161
|
+
min="1"
|
|
162
|
+
max="200"
|
|
163
|
+
bind:value={limit}
|
|
164
|
+
class="w-12 bg-transparent text-right text-fg-1 outline-none"
|
|
165
|
+
/>
|
|
166
|
+
</label>
|
|
167
|
+
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
class={[
|
|
171
|
+
'inline-flex h-7 items-center gap-1 rounded border px-2 transition-colors',
|
|
172
|
+
searching
|
|
173
|
+
? 'border-line-1 bg-bg-2 text-fg-3'
|
|
174
|
+
: 'border-accent/40 bg-accent text-white hover:bg-accent/90',
|
|
175
|
+
].join(' ')}
|
|
176
|
+
disabled={searching}
|
|
177
|
+
onclick={() => void runSearch()}
|
|
178
|
+
>
|
|
179
|
+
{#if searching}
|
|
180
|
+
<LoaderCircle class="size-3 animate-spin" />
|
|
181
|
+
{:else}
|
|
182
|
+
<Play class="size-3" />
|
|
183
|
+
{/if}
|
|
184
|
+
search
|
|
185
|
+
</button>
|
|
186
|
+
{:else}
|
|
187
|
+
<div class="inline-flex items-center gap-1.5 border border-line-1 rounded px-2 py-1 bg-bg-0">
|
|
188
|
+
<Search class="size-3 text-fg-3" />
|
|
189
|
+
<input
|
|
190
|
+
type="text"
|
|
191
|
+
bind:value={filter}
|
|
192
|
+
placeholder="filter rows…"
|
|
193
|
+
class="bg-transparent text-fg-1 outline-none w-[180px] placeholder:text-fg-3"
|
|
194
|
+
/>
|
|
195
|
+
{#if filter}
|
|
196
|
+
<button type="button" onclick={() => (filter = '')} class="text-fg-3 hover:text-fg-1 cursor-pointer" aria-label="Clear vector row filter">
|
|
197
|
+
<X class="size-3" />
|
|
198
|
+
</button>
|
|
199
|
+
{/if}
|
|
200
|
+
</div>
|
|
201
|
+
{/if}
|
|
202
|
+
|
|
203
|
+
<div class="ml-auto flex items-center gap-2 text-fg-3">
|
|
204
|
+
<span>{declaredDimension ? `${declaredDimension}d` : 'dimension n/a'}</span>
|
|
205
|
+
{#if collection}<span>{collection}</span>{/if}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{#if searchError}
|
|
210
|
+
<div class="mt-1.5 text-warn">{searchError}</div>
|
|
211
|
+
{/if}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{#if surface === 'search' && syntheticPreview}
|
|
215
|
+
<div class="border-b border-line-1 bg-bg-1/30 px-3 py-1.5 text-[11px] font-mono text-fg-3">
|
|
216
|
+
Preview search: distances and scores are computed against the synthetic basis vector
|
|
217
|
+
<span class="text-fg-1">[1, 0, 0, …]</span>.
|
|
218
|
+
</div>
|
|
219
|
+
{/if}
|
|
220
|
+
|
|
221
|
+
{#if surface === 'search' && searchRows.length === 0}
|
|
222
|
+
<div class="flex-1 grid place-items-center p-6 text-center font-mono text-[12px] text-fg-3">
|
|
223
|
+
<div>
|
|
224
|
+
<Search class="mx-auto mb-3 size-7 text-fg-3" strokeWidth={1.4} />
|
|
225
|
+
<div class="text-fg-1">{hasSearch ? 'No search results.' : 'No vector search has been run.'}</div>
|
|
226
|
+
<div class="mt-1">Text queries are embedded by RedDB.</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
{:else if rows.length === 0}
|
|
230
|
+
<div class="flex-1 grid place-items-center p-6 text-center font-mono text-[12px] text-fg-3">
|
|
231
|
+
<div>
|
|
232
|
+
<Rows3 class="mx-auto mb-3 size-7 text-fg-3" strokeWidth={1.4} />
|
|
233
|
+
<div class="text-fg-1">No vector rows returned.</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
{:else}
|
|
237
|
+
<div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 text-[11px] font-mono bg-bg-0">
|
|
238
|
+
<div class="inline-flex border border-line-2 rounded overflow-hidden">
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors', mode === 'metadata' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
242
|
+
onclick={() => (mode = 'metadata')}
|
|
243
|
+
aria-pressed={mode === 'metadata'}
|
|
244
|
+
>
|
|
245
|
+
<Braces class="size-3" />
|
|
246
|
+
metadata
|
|
247
|
+
</button>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'vector' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
251
|
+
onclick={() => (mode = 'vector')}
|
|
252
|
+
aria-pressed={mode === 'vector'}
|
|
253
|
+
>
|
|
254
|
+
<ListFilter class="size-3" />
|
|
255
|
+
vector
|
|
256
|
+
</button>
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
class={['inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2', mode === 'scalar' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
|
|
260
|
+
onclick={() => (mode = 'scalar')}
|
|
261
|
+
aria-pressed={mode === 'scalar'}
|
|
262
|
+
>
|
|
263
|
+
<Gauge class="size-3" />
|
|
264
|
+
scores
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{#if surface === 'search'}
|
|
269
|
+
<div class="inline-flex items-center gap-1.5 border border-line-1 rounded px-2 py-1 bg-bg-0">
|
|
270
|
+
<Search class="size-3 text-fg-3" />
|
|
271
|
+
<input
|
|
272
|
+
type="text"
|
|
273
|
+
bind:value={filter}
|
|
274
|
+
placeholder="filter results…"
|
|
275
|
+
class="bg-transparent text-fg-1 outline-none w-[180px] placeholder:text-fg-3"
|
|
276
|
+
/>
|
|
277
|
+
{#if filter}
|
|
278
|
+
<button type="button" onclick={() => (filter = '')} class="text-fg-3 hover:text-fg-1 cursor-pointer" aria-label="Clear vector result filter">
|
|
279
|
+
<X class="size-3" />
|
|
280
|
+
</button>
|
|
281
|
+
{/if}
|
|
282
|
+
</div>
|
|
283
|
+
{/if}
|
|
284
|
+
|
|
285
|
+
<div class="ml-auto text-fg-3">
|
|
286
|
+
{filtered.length.toLocaleString()}{#if filter} of {rows.length.toLocaleString()}{/if}
|
|
287
|
+
{surface === 'search' ? ' results' : ' rows'}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="flex-1 min-h-0 grid grid-cols-[320px_1fr]">
|
|
292
|
+
<div class="overflow-auto border-r border-line-1 text-[12px] font-mono">
|
|
293
|
+
{#each filtered as row (row.id)}
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
onclick={() => (selected = row)}
|
|
297
|
+
class={[
|
|
298
|
+
'w-full text-left px-3 py-2 border-b border-line-1/60 hover:bg-bg-1/50 cursor-pointer',
|
|
299
|
+
active === row ? 'bg-bg-1 text-fg-0' : 'text-fg-2',
|
|
300
|
+
].join(' ')}
|
|
301
|
+
>
|
|
302
|
+
<div class="flex items-baseline gap-2">
|
|
303
|
+
<span class="text-accent">{row.id}</span>
|
|
304
|
+
<span class="text-fg-3">{row.dimension}d</span>
|
|
305
|
+
{#if surface === 'search' && row.scalar !== null}<span class="ml-auto text-fg-1" title={scalarLabel(row)}>{row.scalar.toFixed(4)}</span>{/if}
|
|
306
|
+
</div>
|
|
307
|
+
{#if preview(row)}
|
|
308
|
+
<div class="mt-0.5 truncate text-fg-3">{preview(row)}</div>
|
|
309
|
+
{/if}
|
|
310
|
+
</button>
|
|
311
|
+
{/each}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div class="overflow-auto p-3 text-[12px] font-mono">
|
|
315
|
+
{#if !active}
|
|
316
|
+
<div class="text-fg-3">No vector selected.</div>
|
|
317
|
+
{:else if mode === 'metadata'}
|
|
318
|
+
<div class="grid grid-cols-[160px_1fr] gap-x-3 gap-y-1.5">
|
|
319
|
+
<div class="text-fg-3">id</div>
|
|
320
|
+
<div class="text-fg-0">{active.id}</div>
|
|
321
|
+
<div class="text-fg-3">dimension</div>
|
|
322
|
+
<div class="text-fg-0">{active.dimension}</div>
|
|
323
|
+
<div class="text-fg-3">magnitude</div>
|
|
324
|
+
<div class="text-fg-0">{active.vector.length > 0 ? vectorMagnitude(active.vector).toFixed(6) : 'n/a'}</div>
|
|
325
|
+
<div class="text-fg-3">coordinates</div>
|
|
326
|
+
<div class="text-fg-0">{active.vector.length > 0 ? 'returned' : 'not returned'}</div>
|
|
327
|
+
{#if active.vectorColumn}
|
|
328
|
+
<div class="text-fg-3">vector column</div>
|
|
329
|
+
<div class="text-fg-0">{active.vectorColumn}</div>
|
|
330
|
+
{/if}
|
|
331
|
+
{#if surface === 'search' && active.scalarColumn}
|
|
332
|
+
<div class="text-fg-3">ranking metric</div>
|
|
333
|
+
<div class="text-fg-0">{scalarLabel(active)}</div>
|
|
334
|
+
{/if}
|
|
335
|
+
{#each Object.entries(active.metadata) as [k, v]}
|
|
336
|
+
<div class="text-fg-3 truncate">{k}</div>
|
|
337
|
+
<div class="text-fg-1 break-words">{compactValue(v)}</div>
|
|
338
|
+
{/each}
|
|
339
|
+
</div>
|
|
340
|
+
{:else if mode === 'vector'}
|
|
341
|
+
{#if active.vector.length === 0}
|
|
342
|
+
<div class="border border-line-1 bg-bg-1/40 px-3 py-2 text-fg-3">
|
|
343
|
+
This result reports a {active.dimension}d vector, but the coordinate array was not returned by RedDB.
|
|
344
|
+
</div>
|
|
345
|
+
{:else}
|
|
346
|
+
<div class="mb-2 flex items-center justify-between text-fg-3">
|
|
347
|
+
<span>{active.vector.length} dimensions</span>
|
|
348
|
+
<span>|v| {vectorMagnitude(active.vector).toFixed(6)} / max visible {maxMagnitude.toFixed(2)}</span>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="grid gap-1">
|
|
351
|
+
{#each active.vector.slice(0, 256) as value, i}
|
|
352
|
+
<div class="grid grid-cols-[52px_1fr_90px] items-center gap-2">
|
|
353
|
+
<span class="text-fg-3 text-right">{i}</span>
|
|
354
|
+
<div class="h-2 bg-bg-2 rounded overflow-hidden">
|
|
355
|
+
<div class="h-full bg-accent/80" style:width={`${barWidth(value, active.vector)}%`}></div>
|
|
356
|
+
</div>
|
|
357
|
+
<span class={value < 0 ? 'text-warn text-right' : 'text-fg-1 text-right'}>{value.toFixed(5)}</span>
|
|
358
|
+
</div>
|
|
359
|
+
{/each}
|
|
360
|
+
</div>
|
|
361
|
+
{#if active.vector.length > 256}
|
|
362
|
+
<div class="mt-2 text-fg-3">Showing first 256 dimensions.</div>
|
|
363
|
+
{/if}
|
|
364
|
+
{/if}
|
|
365
|
+
{:else}
|
|
366
|
+
<table class="w-full border-collapse">
|
|
367
|
+
<thead class="sticky top-0 bg-bg-0">
|
|
368
|
+
<tr class="border-b border-line-1 text-fg-3">
|
|
369
|
+
<th class="px-2 py-1.5 text-left font-normal">id</th>
|
|
370
|
+
<th class="px-2 py-1.5 text-right font-normal">ranking metric</th>
|
|
371
|
+
<th class="px-2 py-1.5 text-right font-normal">magnitude</th>
|
|
372
|
+
</tr>
|
|
373
|
+
</thead>
|
|
374
|
+
<tbody>
|
|
375
|
+
{#each filtered as row (row.id)}
|
|
376
|
+
<tr class="border-b border-line-1/60 hover:bg-bg-1/40 cursor-pointer" onclick={() => (selected = row)}>
|
|
377
|
+
<td class="px-2 py-1 text-accent">{row.id}</td>
|
|
378
|
+
<td class="px-2 py-1 text-right" title={scalarLabel(row)}>{row.scalar === null ? 'n/a' : row.scalar.toFixed(6)}</td>
|
|
379
|
+
<td class="px-2 py-1 text-right">{vectorMagnitude(row.vector).toFixed(6)}</td>
|
|
380
|
+
</tr>
|
|
381
|
+
{/each}
|
|
382
|
+
</tbody>
|
|
383
|
+
</table>
|
|
384
|
+
{/if}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
{/if}
|
|
388
|
+
</div>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { QueryResult } from '#reddb'
|
|
2
|
+
|
|
3
|
+
export interface DiffEntry {
|
|
4
|
+
index: number
|
|
5
|
+
id: string
|
|
6
|
+
change: string
|
|
7
|
+
collection: string | null
|
|
8
|
+
entityId: string | null
|
|
9
|
+
before: unknown
|
|
10
|
+
after: unknown
|
|
11
|
+
note: string | null
|
|
12
|
+
values: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DiffFieldChange {
|
|
16
|
+
field: string
|
|
17
|
+
before: unknown
|
|
18
|
+
after: unknown
|
|
19
|
+
status: 'added' | 'removed' | 'modified' | 'unchanged'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CHANGE_COLUMNS = ['change', 'change_type', 'operation', 'op']
|
|
23
|
+
const COLLECTION_COLUMNS = ['target_collection', 'collection_name', 'diff_collection']
|
|
24
|
+
const ENTITY_COLUMNS = ['entity_id', 'row_id', 'rid', 'id']
|
|
25
|
+
const BEFORE_COLUMNS = ['before_state', 'before_row', 'before']
|
|
26
|
+
const AFTER_COLUMNS = ['after_state', 'after_row', 'after']
|
|
27
|
+
const NOTE_COLUMNS = ['note', 'summary', 'message']
|
|
28
|
+
|
|
29
|
+
function pick(values: Record<string, unknown>, names: string[]): [string, unknown] | null {
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
if (name in values) return [name, values[name]]
|
|
32
|
+
}
|
|
33
|
+
return Object.entries(values).find(([k]) => names.includes(k.toLowerCase())) ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseMaybeJson(value: unknown): unknown {
|
|
37
|
+
if (typeof value !== 'string') return value
|
|
38
|
+
const trimmed = value.trim()
|
|
39
|
+
if (!trimmed || !/^[{[]/.test(trimmed)) return value
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(trimmed)
|
|
42
|
+
} catch {
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function extractDiffEntries(result: QueryResult): DiffEntry[] {
|
|
48
|
+
return result.result.records.map((record, index) => {
|
|
49
|
+
const values = record.values ?? {}
|
|
50
|
+
const change = pick(values, CHANGE_COLUMNS)
|
|
51
|
+
const collection = pick(values, COLLECTION_COLUMNS)
|
|
52
|
+
const entity = pick(values, ENTITY_COLUMNS)
|
|
53
|
+
const before = pick(values, BEFORE_COLUMNS)
|
|
54
|
+
const after = pick(values, AFTER_COLUMNS)
|
|
55
|
+
const note = pick(values, NOTE_COLUMNS)
|
|
56
|
+
return {
|
|
57
|
+
index,
|
|
58
|
+
id: String(values.id ?? values.diff_id ?? entity?.[1] ?? index + 1),
|
|
59
|
+
change: String(change?.[1] ?? 'modified'),
|
|
60
|
+
collection: collection?.[1] === undefined || collection?.[1] === null ? null : String(collection[1]),
|
|
61
|
+
entityId: entity?.[1] === undefined || entity?.[1] === null ? null : String(entity[1]),
|
|
62
|
+
before: parseMaybeJson(before?.[1] ?? null),
|
|
63
|
+
after: parseMaybeJson(after?.[1] ?? null),
|
|
64
|
+
note: note?.[1] === undefined || note?.[1] === null ? null : String(note[1]),
|
|
65
|
+
values,
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hasDiffShape(result: QueryResult): boolean {
|
|
71
|
+
if (result.capability === 'diff') return true
|
|
72
|
+
return result.result.records.some((record) => {
|
|
73
|
+
const keys = Object.keys(record.values ?? {}).map((k) => k.toLowerCase())
|
|
74
|
+
return CHANGE_COLUMNS.some((c) => keys.includes(c)) &&
|
|
75
|
+
(BEFORE_COLUMNS.some((c) => keys.includes(c)) || AFTER_COLUMNS.some((c) => keys.includes(c)))
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function summarizeDiff(entries: DiffEntry[]) {
|
|
80
|
+
const counts = new Map<string, number>()
|
|
81
|
+
for (const entry of entries) counts.set(entry.change, (counts.get(entry.change) ?? 0) + 1)
|
|
82
|
+
return [...counts.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
86
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stableValue(value: unknown): string {
|
|
90
|
+
if (value === undefined) return '__red_ui_undefined__'
|
|
91
|
+
if (isPlainRecord(value)) {
|
|
92
|
+
return JSON.stringify(Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))))
|
|
93
|
+
}
|
|
94
|
+
return JSON.stringify(value)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function diffEntryFields(entry: DiffEntry): DiffFieldChange[] {
|
|
98
|
+
const before = isPlainRecord(entry.before) ? entry.before : {}
|
|
99
|
+
const after = isPlainRecord(entry.after) ? entry.after : {}
|
|
100
|
+
const fields = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
101
|
+
|
|
102
|
+
return [...fields].sort((a, b) => a.localeCompare(b)).map((field) => {
|
|
103
|
+
const beforeHas = Object.prototype.hasOwnProperty.call(before, field)
|
|
104
|
+
const afterHas = Object.prototype.hasOwnProperty.call(after, field)
|
|
105
|
+
const previous = before[field]
|
|
106
|
+
const next = after[field]
|
|
107
|
+
const status = !beforeHas && afterHas
|
|
108
|
+
? 'added'
|
|
109
|
+
: beforeHas && !afterHas
|
|
110
|
+
? 'removed'
|
|
111
|
+
: stableValue(previous) === stableValue(next)
|
|
112
|
+
? 'unchanged'
|
|
113
|
+
: 'modified'
|
|
114
|
+
return { field, before: previous, after: next, status }
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatDiffValue(value: unknown): string {
|
|
119
|
+
if (value === null || value === undefined || value === '') return '-'
|
|
120
|
+
if (typeof value === 'object') return JSON.stringify(value, null, 2)
|
|
121
|
+
return String(value)
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import DiffRenderer from './DiffRenderer.svelte'
|
|
2
|
+
import { hasDiffShape } from './diff-render'
|
|
3
|
+
import type { CapabilityRenderer } from './types'
|
|
4
|
+
|
|
5
|
+
export const diffRenderer: CapabilityRenderer = {
|
|
6
|
+
capability: 'diff',
|
|
7
|
+
renders: hasDiffShape,
|
|
8
|
+
component: DiffRenderer,
|
|
9
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { QueryResult } from '#reddb'
|
|
2
|
+
|
|
3
|
+
const SYSTEM_FIELDS = new Set(['rid', 'collection', 'kind', 'tenant', 'created_at', 'updated_at', 'body'])
|
|
4
|
+
|
|
5
|
+
export interface DocumentItem {
|
|
6
|
+
rid: string
|
|
7
|
+
title: string
|
|
8
|
+
subtitle: string
|
|
9
|
+
body: unknown
|
|
10
|
+
fields: Record<string, unknown>
|
|
11
|
+
values: Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function titleFrom(values: Record<string, unknown>, body: unknown, index: number): string {
|
|
15
|
+
const bodyObject = body && typeof body === 'object' && !Array.isArray(body) ? body as Record<string, unknown> : {}
|
|
16
|
+
for (const key of ['title', 'name', 'slug', 'id', 'event_type', 'type']) {
|
|
17
|
+
const v = values[key] ?? bodyObject[key]
|
|
18
|
+
if (typeof v === 'string' && v.trim()) return v
|
|
19
|
+
if (typeof v === 'number') return String(v)
|
|
20
|
+
}
|
|
21
|
+
const rid = values.rid
|
|
22
|
+
if (typeof rid === 'string' || typeof rid === 'number') return `document ${rid}`
|
|
23
|
+
return `document ${index + 1}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function subtitleFrom(values: Record<string, unknown>, body: unknown): string {
|
|
27
|
+
const bodyObject = body && typeof body === 'object' && !Array.isArray(body) ? body as Record<string, unknown> : {}
|
|
28
|
+
const parts: string[] = []
|
|
29
|
+
for (const key of ['category', 'source', 'status', 'collection']) {
|
|
30
|
+
const v = values[key] ?? bodyObject[key]
|
|
31
|
+
if (typeof v === 'string' && v.trim()) parts.push(v)
|
|
32
|
+
}
|
|
33
|
+
const updated = values.updated_at ?? values.created_at
|
|
34
|
+
if (typeof updated === 'string' || typeof updated === 'number') parts.push(String(updated))
|
|
35
|
+
return parts.join(' · ')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractDocuments(result: QueryResult): DocumentItem[] {
|
|
39
|
+
const docs: DocumentItem[] = []
|
|
40
|
+
for (const [index, record] of result.result.records.entries()) {
|
|
41
|
+
const values = record.values ?? {}
|
|
42
|
+
const body = values.body
|
|
43
|
+
if (values.kind !== 'document' && (body === null || typeof body !== 'object')) continue
|
|
44
|
+
const fields: Record<string, unknown> = {}
|
|
45
|
+
for (const [key, value] of Object.entries(values)) {
|
|
46
|
+
if (SYSTEM_FIELDS.has(key.toLowerCase())) continue
|
|
47
|
+
fields[key] = value
|
|
48
|
+
}
|
|
49
|
+
docs.push({
|
|
50
|
+
rid: String(values.rid ?? index + 1),
|
|
51
|
+
title: titleFrom(values, body, index),
|
|
52
|
+
subtitle: subtitleFrom(values, body),
|
|
53
|
+
body,
|
|
54
|
+
fields,
|
|
55
|
+
values,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return docs
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasDocumentShape(result: QueryResult): boolean {
|
|
62
|
+
if (result.capability === 'document') return true
|
|
63
|
+
return extractDocuments(result).length > 0
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CapabilityRenderer } from './types'
|
|
2
|
+
import DocumentRenderer from './DocumentRenderer.svelte'
|
|
3
|
+
import { hasDocumentShape } from './document-render'
|
|
4
|
+
|
|
5
|
+
export const documentRenderer: CapabilityRenderer = {
|
|
6
|
+
capability: 'document',
|
|
7
|
+
renders: hasDocumentShape,
|
|
8
|
+
component: DocumentRenderer,
|
|
9
|
+
}
|