|
|
@@ -0,0 +1,137 @@
|
|
|
+<script lang="ts">
|
|
|
+ import { CircleCheckBig } from '@lucide/svelte';
|
|
|
+
|
|
|
+ import * as kbsApi from '$lib/api/kbs';
|
|
|
+ import { GlassPane } from '$lib/components/common/glass-pane';
|
|
|
+ import { TestResult } from '$lib/components/common/test-result';
|
|
|
+ import { Button } from '$lib/components/controls/button';
|
|
|
+ import * as Field from '$lib/components/controls/field';
|
|
|
+ import { Input } from '$lib/components/controls/input';
|
|
|
+ import { Optional } from '$lib/components/forms/optional';
|
|
|
+ import type { KnowledgeBase } from '$lib/types/entities';
|
|
|
+
|
|
|
+ let { workspaceId, kb }: { workspaceId: string; kb: KnowledgeBase } = $props();
|
|
|
+
|
|
|
+ let query = $state('');
|
|
|
+ let topK = $state(5);
|
|
|
+ let threshold = $state(0.7);
|
|
|
+
|
|
|
+ let response = $state<kbsApi.KBQueryResponse | undefined>(undefined);
|
|
|
+ let loading = $state(false);
|
|
|
+ let error = $state<{ type: string; message: string } | undefined>(undefined);
|
|
|
+ const queryResponse = $derived(response?.response);
|
|
|
+
|
|
|
+ $effect(() => {
|
|
|
+ if (kb) {
|
|
|
+ query = '';
|
|
|
+ topK = 5;
|
|
|
+ threshold = 0.7;
|
|
|
+ response = undefined;
|
|
|
+ error = undefined;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const runQuery = async () => {
|
|
|
+ if (!query || !query.trim() || loading) return;
|
|
|
+ error = undefined;
|
|
|
+ response = undefined;
|
|
|
+ loading = true;
|
|
|
+ try {
|
|
|
+ response = await kbsApi.testQuery(workspaceId, kb.id, {
|
|
|
+ query: query.trim(),
|
|
|
+ topK,
|
|
|
+ threshold,
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ error =
|
|
|
+ err instanceof Error
|
|
|
+ ? { type: 'Request Error', message: err.message }
|
|
|
+ : { type: 'Unknown Error', message: 'An error occurred' };
|
|
|
+ } finally {
|
|
|
+ loading = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const scoreColor = (score: number) =>
|
|
|
+ score >= 0.7
|
|
|
+ ? 'bg-green-500/20 text-green-300 border-green-500/30'
|
|
|
+ : score >= 0.4
|
|
|
+ ? 'bg-amber-500/20 text-amber-300 border-amber-500/30'
|
|
|
+ : 'bg-red-500/20 text-red-300 border-red-500/30';
|
|
|
+</script>
|
|
|
+
|
|
|
+<GlassPane class="overflow-hidden bg-black/25">
|
|
|
+ <div class="bg-white/5 p-4">
|
|
|
+ <div class="mb-4 flex items-center justify-between">
|
|
|
+ <p class="text-lg font-thin">Query</p>
|
|
|
+ <Button disabled={!query.trim() || loading} onclick={() => runQuery()}>Query</Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Optional title="Parameters">
|
|
|
+ <div class="grid grid-cols-2 gap-3">
|
|
|
+ <Field.Field>
|
|
|
+ <Field.Label for="topK">Top K</Field.Label>
|
|
|
+ <Input id="topK" max={100} min={1} type="number" bind:value={topK} />
|
|
|
+ </Field.Field>
|
|
|
+ <Field.Field>
|
|
|
+ <Field.Label for="threshold">Threshold</Field.Label>
|
|
|
+ <Input id="threshold" max={1} min={0} step={0.05} type="number" bind:value={threshold} />
|
|
|
+ </Field.Field>
|
|
|
+ </div>
|
|
|
+ </Optional>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <textarea
|
|
|
+ class="h-full w-full resize-none p-4 font-mono text-sm outline-none"
|
|
|
+ placeholder="Enter your query..."
|
|
|
+ rows={5}
|
|
|
+ bind:value={query}
|
|
|
+ >
|
|
|
+ </textarea>
|
|
|
+
|
|
|
+ <div class="bg-white/5 p-4">
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <span class="text-lg font-thin">Result</span>
|
|
|
+ {#if response}
|
|
|
+ <div class="flex items-center gap-2 text-sm">
|
|
|
+ <CircleCheckBig class="text-green-500" size={14} />
|
|
|
+ {#if response.latencyMs}
|
|
|
+ <span class="text-white/25">Latency:</span>
|
|
|
+ <span class="text-white/75">{response.latencyMs}ms</span>
|
|
|
+ {/if}
|
|
|
+ {#if queryResponse}
|
|
|
+ <span class="text-white/25">Chunks:</span>
|
|
|
+ <span class="text-white/75">{queryResponse.chunks.length}</span>
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <TestResult class="bg-black/20" {error} {loading} {response}>
|
|
|
+ {#snippet result(resp)}
|
|
|
+ {#if resp}
|
|
|
+ {#if resp.chunks.length === 0}
|
|
|
+ <p class="text-center text-xs text-white/20">No results</p>
|
|
|
+ {:else}
|
|
|
+ <div>
|
|
|
+ {#each resp.chunks as chunk, rank (rank)}
|
|
|
+ <div class="border-b border-white/10 p-4 last:border-0">
|
|
|
+ <div class="mb-2 flex items-center gap-2">
|
|
|
+ <span class="shrink-0 font-mono text-sm text-white/50">#{rank + 1}</span>
|
|
|
+ <span
|
|
|
+ class={`shrink-0 rounded border px-2 py-1 font-mono text-xs ${scoreColor(chunk.score)}`}
|
|
|
+ >
|
|
|
+ {chunk.score.toFixed(3)}
|
|
|
+ </span>
|
|
|
+ <span class="truncate text-sm text-white/75">{chunk.documentId}</span>
|
|
|
+ </div>
|
|
|
+ <p class="text-sm text-white/75">{chunk.content}</p>
|
|
|
+ </div>
|
|
|
+ {/each}
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
+ {/if}
|
|
|
+ {/snippet}
|
|
|
+ </TestResult>
|
|
|
+</GlassPane>
|