rerank.svelte 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <script lang="ts">
  2. import { CircleCheckBig, Plus, X } from '@lucide/svelte';
  3. import * as modelsApi from '$lib/api/models';
  4. import { GlassPane } from '$lib/components/common/glass-pane';
  5. import { TestResult } from '$lib/components/common/test-result';
  6. import { Button } from '$lib/components/controls/button';
  7. import { Spinner } from '$lib/components/controls/spinner';
  8. import { Textarea } from '$lib/components/controls/textarea';
  9. import type { Model } from '$lib/types/entities';
  10. type TestDoc = { id: number; text: string; score?: number };
  11. let { workspaceId, model }: { workspaceId: string; model: Model } = $props();
  12. let query = $state('wireless noise cancelling headphones');
  13. let docId = $state(3);
  14. let docs = $state<TestDoc[]>([
  15. { id: 0, text: 'Fresh apples are available at the local farmers market.' },
  16. { id: 1, text: 'This speaker delivers powerful sound for outdoor parties.' },
  17. { id: 2, text: 'These headphones offer active noise cancellation and Bluetooth connectivity.' },
  18. ]);
  19. let response = $state<modelsApi.RerankResponse | undefined>(undefined);
  20. let loading = $state(false);
  21. let error = $state<{ type: string; message: string } | undefined>(undefined);
  22. const rerankResponse = $derived(response?.response);
  23. $effect(() => {
  24. if (model) {
  25. response = undefined;
  26. error = undefined;
  27. }
  28. });
  29. const runRerank = async () => {
  30. if (!model || !query.trim() || loading) return;
  31. if (!query.trim() || docs.filter((d) => d.text.trim()).length < 2) return;
  32. error = undefined;
  33. response = undefined;
  34. loading = true;
  35. try {
  36. response = await modelsApi.testRerank(workspaceId, model.id, {
  37. query: query.trim(),
  38. documents: docs.map((d) => d.text.trim()).filter((d) => d),
  39. });
  40. for (const item of response?.response?.results || []) {
  41. docs[item.index].score = item.relevance_score;
  42. }
  43. docs = [...docs].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
  44. } catch (err) {
  45. error =
  46. err instanceof Error
  47. ? { type: 'error', message: err.message }
  48. : { type: 'error', message: 'An error occurred' };
  49. } finally {
  50. loading = false;
  51. }
  52. };
  53. const addDoc = () => {
  54. docs = [...docs, { id: docId, text: '' }];
  55. docId += 1;
  56. };
  57. const removeDoc = (idx: number) => {
  58. if (docs.length <= 2) return;
  59. docs = docs.filter((_, i) => i !== idx);
  60. };
  61. </script>
  62. <GlassPane class="overflow-hidden bg-black/25">
  63. <div class="bg-white/5 p-4">
  64. <div class="flex items-center justify-between">
  65. <span class="text-lg font-thin">Rerank</span>
  66. <Button
  67. disabled={!query.trim() || docs.filter((d) => d.text.trim()).length < 2 || loading}
  68. title="Rerank"
  69. onclick={runRerank}
  70. >
  71. {#if loading}
  72. <Spinner />
  73. {/if}
  74. Rerank
  75. </Button>
  76. </div>
  77. </div>
  78. <textarea
  79. class="h-full w-full resize-none p-4 font-mono text-sm outline-none"
  80. placeholder="Enter query..."
  81. rows={2}
  82. bind:value={query}
  83. >
  84. </textarea>
  85. <div class="bg-white/5 p-4">
  86. <div class="flex items-center justify-between">
  87. <span class="text-lg font-thin">Documents</span>
  88. {#if response}
  89. <div class="flex items-center gap-2 text-sm">
  90. <CircleCheckBig class="text-green-500" size={14} />
  91. {#if response.latencyMs}
  92. <span class="text-white/25">Latency:</span>
  93. <span class="text-white/75">{response.latencyMs}ms</span>
  94. {/if}
  95. {#if rerankResponse}
  96. <span class="text-white/25">Prompt tokens:</span>
  97. <span class="text-white/75">
  98. {rerankResponse.meta?.tokens.input_tokens ?? rerankResponse.usage?.prompt_tokens}
  99. </span>
  100. <span class="text-white/25">Total tokens:</span>
  101. <span class="text-white/75">
  102. {rerankResponse.meta?.billed_units.total_tokens ?? rerankResponse.usage?.total_tokens}
  103. </span>
  104. {/if}
  105. </div>
  106. {/if}
  107. </div>
  108. </div>
  109. {#each docs as doc, idx (doc.id)}
  110. <div class="flex items-start gap-2 border-b border-white/5 p-2 py-4 last:border-b-0">
  111. <div class="flex w-12 flex-col items-center justify-center">
  112. {#if doc.score !== undefined}
  113. <div class="text-sm text-white/50">#{idx + 1}</div>
  114. <div class="line-clamp-3 text-center font-mono text-xs break-all">
  115. {doc.score.toFixed(10)}
  116. </div>
  117. {/if}
  118. </div>
  119. <Textarea
  120. class="flex-1 resize-none text-sm"
  121. placeholder="Paste document here..."
  122. rows={2}
  123. bind:value={doc.text}
  124. />
  125. <button
  126. class="mt-1 shrink-0 opacity-25 hover:opacity-75 disabled:cursor-not-allowed disabled:opacity-20"
  127. disabled={docs.length <= 2}
  128. onclick={() => removeDoc(idx)}
  129. >
  130. <X size={14} strokeWidth={2} />
  131. </button>
  132. </div>
  133. {/each}
  134. <div class="p-2 text-right">
  135. <Button variant="ghost" onclick={() => addDoc()}>
  136. <Plus size={14} strokeWidth={2} />
  137. Add
  138. </Button>
  139. </div>
  140. <TestResult {error} {loading} {response}>
  141. {#snippet result()}
  142. <div class="flex h-48 items-center justify-center text-sm text-white/50">No errors</div>
  143. {/snippet}
  144. </TestResult>
  145. </GlassPane>