This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"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 { AiDisclosureBadge, type ProjectSlugs, useTransport } from "@conusai-kit/ui"
|
||||
import { type FormEvent, type ReactElement, useId, useState } from "react"
|
||||
import type { SearchResult } from "../contract/index.js"
|
||||
import type { SearchFormLabels } from "./labels.js"
|
||||
|
||||
type State =
|
||||
| { kind: "idle" }
|
||||
| { kind: "busy" }
|
||||
| { kind: "done"; results: SearchResult[] }
|
||||
| { kind: "failed"; detail: string }
|
||||
|
||||
export function SearchForm({
|
||||
slugs,
|
||||
collectionName,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
collectionName: string
|
||||
labels: SearchFormLabels
|
||||
}): ReactElement {
|
||||
const { call, describe } = useTransport()
|
||||
const inputId = useId()
|
||||
const [state, setState] = useState<State>({ kind: "idle" })
|
||||
|
||||
const search = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event.preventDefault()
|
||||
const query = String(new FormData(event.currentTarget).get("query") ?? "").trim()
|
||||
if (query === "") return
|
||||
setState({ kind: "busy" })
|
||||
try {
|
||||
const answer = (await call("knowledge.search", { ...slugs, collectionName, query })) as {
|
||||
results: SearchResult[]
|
||||
}
|
||||
setState({ kind: "done", results: answer.results })
|
||||
} catch (error) {
|
||||
setState({ kind: "failed", detail: describe(error) })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-labelledby={`${inputId}-heading`}>
|
||||
<h2 id={`${inputId}-heading`}>{labels.heading}</h2>
|
||||
<AiDisclosureBadge label={labels.disclosure} />
|
||||
<form onSubmit={(event) => void search(event)}>
|
||||
<label htmlFor={inputId}>{labels.label}</label>
|
||||
<input id={inputId} name="query" type="search" placeholder={labels.placeholder} required maxLength={500} />
|
||||
<button type="submit" disabled={state.kind === "busy"}>
|
||||
{state.kind === "busy" ? labels.busy : labels.submit}
|
||||
</button>
|
||||
</form>
|
||||
{state.kind === "failed" ? <p role="alert">{labels.failed.replace("{detail}", state.detail)}</p> : null}
|
||||
{state.kind === "done" ? (
|
||||
state.results.length === 0 ? (
|
||||
<p>{labels.empty}</p>
|
||||
) : (
|
||||
<ol aria-label={labels.results.replace("{count}", String(state.results.length))}>
|
||||
{state.results.map((result, at) => (
|
||||
<li key={`${result.fileId}-${result.chunk}-${at}`}>
|
||||
<blockquote>{result.snippet}</blockquote>
|
||||
<p>
|
||||
{labels.source}: {result.filename} · {labels.chunk.replace("{chunk}", String(result.chunk))}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user