92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
"use client"
|
|
|
|
// The search form: one question, cited answers. The disclosure the EU AI Act Article 50 asks for
|
|
// rides with the results, and every answer names its source and chunk — a person checks a passage
|
|
// against the file, never a model's word.
|
|
|
|
import { formatMessage } from "@conusai-kit/i18n/format"
|
|
import { AiDisclosureBadge, type ProjectSlugs } from "@conusai-kit/ui"
|
|
import { useDataClient } from "@conusai-kit/ui/data"
|
|
import { FormLayout, type FormValues, PageSection, TextField } from "@conusai-kit/ui/patterns"
|
|
import { Alert, AlertDescription } from "@conusai-kit/ui/primitives"
|
|
import { type ReactElement, useState } from "react"
|
|
import type { SearchResult } from "../contract/index.js"
|
|
import { type RefusalNotice, refusalOf, type SearchFormLabels } from "./labels.js"
|
|
|
|
export function SearchForm({
|
|
slugs,
|
|
collectionName,
|
|
labels,
|
|
locale,
|
|
}: {
|
|
slugs: ProjectSlugs
|
|
collectionName: string
|
|
labels: SearchFormLabels
|
|
locale: string
|
|
}): ReactElement {
|
|
const client = useDataClient()
|
|
const [results, setResults] = useState<SearchResult[] | null>(null)
|
|
const [refused, setRefused] = useState<RefusalNotice | undefined>(undefined)
|
|
|
|
const search = async ({ query = "" }: FormValues): Promise<void> => {
|
|
setResults(null)
|
|
setRefused(undefined)
|
|
try {
|
|
const { value: answer } = await client.call<{ results: SearchResult[] }>("knowledge.search", {
|
|
...slugs,
|
|
collectionName,
|
|
query,
|
|
})
|
|
setResults(answer.results)
|
|
} catch (error) {
|
|
// A refusal of the service has its own words. Anything else is FormLayout's to show.
|
|
const notice = refusalOf(labels.refusals, error)
|
|
if (notice === undefined) throw error
|
|
setRefused(notice)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<PageSection title={labels.heading} level={3} variant="card">
|
|
<AiDisclosureBadge label={labels.disclosure} />
|
|
<FormLayout submitLabel={labels.submit} busyLabel={labels.busy} onSubmit={search}>
|
|
<TextField
|
|
name="query"
|
|
type="search"
|
|
label={labels.label}
|
|
placeholder={labels.placeholder}
|
|
required
|
|
maxLength={500}
|
|
/>
|
|
</FormLayout>
|
|
{refused ? (
|
|
<Alert variant="warning" data-state={refused.code}>
|
|
<AlertDescription>{refused.text}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
{results === null ? null : results.length === 0 ? (
|
|
<p>{labels.empty}</p>
|
|
) : (
|
|
<ol aria-label={formatMessage(labels.results, { count: results.length }, locale)}>
|
|
{results.map((result, at) => (
|
|
<li key={`${result.fileId}-${result.chunk}-${at}`}>
|
|
<blockquote
|
|
data-knowledge-chunk={result.chunkId}
|
|
data-source-page={result.page}
|
|
data-source-block={result.blockId}
|
|
>
|
|
{result.snippet}
|
|
</blockquote>
|
|
<p>
|
|
{labels.source}: {result.filename} · {labels.chunk.replace("{chunk}", String(result.chunk))}
|
|
{result.page === undefined ? null : ` · ${labels.page.replace("{page}", String(result.page))}`}
|
|
{result.blockId ? <code> {result.blockId}</code> : null}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)}
|
|
</PageSection>
|
|
)
|
|
}
|