Files
trueref/docs/features/TRUEREF-0016.md
2026-03-27 02:23:01 +01:00

6.2 KiB

TRUEREF-0016 — Web UI: Search Explorer

Priority: P2 Status: Pending Depends On: TRUEREF-0010, TRUEREF-0015 Blocks:


Overview

An interactive search interface within the web UI that lets users test the documentation retrieval system directly from the browser. Mirrors the two-step context7 flow: first resolve a library ID, then query documentation. Results are displayed with syntax highlighting. Useful for validating indexing quality and demonstrating the system to stakeholders.


Acceptance Criteria

  • Search page at /search with two-step workflow
  • Step 1: Library name input → displays matching libraries with IDs and scores
  • Step 2: Click library → query input → displays ranked snippets
  • Syntax-highlighted code blocks (using a lightweight highlighter)
  • Snippet type badge (code vs info)
  • Breadcrumb display per snippet
  • Token count per snippet and total
  • "Copy as Markdown" button for the full response
  • Library switcher (return to step 1 without full page reload)
  • URL reflects current state (/search?lib=/facebook/react&q=useState)
  • No server-side rendering needed for this page (can be client-side)

Page Layout

/search
├── Header: "Search Documentation"
├── Step 1: Library Search
│   ├── Input: "Library name..." + Search button
│   └── Results list: library cards with ID, description, snippet count, trust score
│       └── [Click to select]
├── Step 2: Documentation Query (shown after library selected)
│   ├── Selected library badge + "Change" button
│   ├── Input: "What would you like to know?" + Search button
│   └── Results:
│       ├── Token count summary
│       ├── "Copy as Markdown" button
│       └── Snippet list:
│           ├── Code snippets: syntax-highlighted code block
│           └── Info snippets: formatted markdown
└── (loading states + empty states throughout)

Component: LibrarySearchResult

<!-- src/lib/components/search/LibraryResult.svelte -->
<script lang="ts">
	let { result, onSelect } = $props<{
		result: {
			id: string;
			title: string;
			description: string;
			totalSnippets: number;
			trustScore: number;
		};
		onSelect: (id: string) => void;
	}>();
</script>

<button
	onclick={() => onSelect(result.id)}
	class="w-full rounded-xl border border-gray-200 bg-white p-4 text-left shadow-sm transition-all hover:border-blue-300 hover:shadow-md"
>
	<div class="flex items-center justify-between">
		<span class="font-semibold text-gray-900">{result.title}</span>
		<span class="text-xs text-gray-400">Trust {result.trustScore.toFixed(1)}/10</span>
	</div>
	<p class="font-mono text-xs text-gray-400">{result.id}</p>
	{#if result.description}
		<p class="mt-1.5 line-clamp-2 text-sm text-gray-600">{result.description}</p>
	{/if}
	<p class="mt-2 text-xs text-gray-400">{result.totalSnippets.toLocaleString()} snippets</p>
</button>

Component: SnippetCard

<!-- src/lib/components/search/SnippetCard.svelte -->
<script lang="ts">
	import type { Snippet } from '$lib/types';

	let { snippet } = $props<{ snippet: Snippet }>();
</script>

<div class="overflow-hidden rounded-xl border border-gray-200 bg-white">
	<div class="flex items-center justify-between border-b border-gray-100 px-4 py-2.5">
		<div class="flex items-center gap-2">
			{#if snippet.type === 'code'}
				<span class="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700">code</span>
			{:else}
				<span class="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">info</span>
			{/if}
			{#if snippet.title}
				<span class="text-sm font-medium text-gray-800">{snippet.title}</span>
			{/if}
		</div>
		<span class="text-xs text-gray-400">{snippet.tokenCount} tokens</span>
	</div>

	{#if snippet.breadcrumb}
		<p class="bg-gray-50 px-4 py-1.5 text-xs text-gray-500 italic">{snippet.breadcrumb}</p>
	{/if}

	<div class="p-4">
		{#if snippet.type === 'code'}
			<pre class="overflow-x-auto rounded bg-gray-950 p-4 text-sm text-gray-100"><code
					>{snippet.content}</code
				></pre>
		{:else}
			<div class="prose prose-sm max-w-none text-gray-700">{snippet.content}</div>
		{/if}
	</div>
</div>

Search Page Logic

<!-- src/routes/search/+page.svelte -->
<script lang="ts">
	import { page } from '$app/stores';
	import { goto } from '$app/navigation';

	let libraryName = $state('');
	let selectedLibraryId = $state<string | null>(null);
	let query = $state('');
	let libraryResults = $state<LibrarySearchResult[]>([]);
	let snippets = $state<Snippet[]>([]);
	let loadingLibraries = $state(false);
	let loadingSnippets = $state(false);

	async function searchLibraries() {
		loadingLibraries = true;
		const res = await fetch(
			`/api/v1/libs/search?libraryName=${encodeURIComponent(libraryName)}&query=${encodeURIComponent(query)}`
		);
		const data = await res.json();
		libraryResults = data.results;
		loadingLibraries = false;
	}

	async function searchDocs() {
		if (!selectedLibraryId) return;
		loadingSnippets = true;
		const url = new URL('/api/v1/context', window.location.origin);
		url.searchParams.set('libraryId', selectedLibraryId);
		url.searchParams.set('query', query);
		const res = await fetch(url);
		const data = await res.json();
		snippets = data.snippets;
		loadingSnippets = false;

		// Update URL
		goto(`/search?lib=${encodeURIComponent(selectedLibraryId)}&q=${encodeURIComponent(query)}`, {
			replaceState: true,
			keepFocus: true
		});
	}
</script>

Syntax Highlighting

Use a minimal, zero-dependency approach for v1 — wrap code blocks in <pre><code> with a CSS-based theme. Optionally integrate highlight.js (lightweight) as an enhancement:

// Optional: lazy-load highlight.js only when code snippets are present
async function highlightCode(code: string, language: string): Promise<string> {
	const hljs = await import('highlight.js/lib/core');
	// Register only needed languages
	return hljs.highlight(code, { language }).value;
}

Files to Create

  • src/routes/search/+page.svelte
  • src/lib/components/search/LibraryResult.svelte
  • src/lib/components/search/SnippetCard.svelte
  • src/lib/components/search/SearchInput.svelte
  • src/lib/utils/copy-to-clipboard.ts