@@ -0,0 +1,91 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user