answers: collapsing behavior fixed height
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { Search } from "lucide-preact";
|
import { Search } from "lucide-preact";
|
||||||
import type { SearchDetail, SearchResultItem } from "@/lib/api";
|
import type { SearchDetail, SearchResultItem } from "@/lib/api";
|
||||||
import { MarkdownContent } from "@/components/markdown/markdown-content";
|
import { MarkdownContent } from "@/components/markdown/markdown-content";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function cleanResultText(input: string) {
|
function cleanResultText(input: string) {
|
||||||
return input
|
return input
|
||||||
@@ -50,6 +52,29 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function SearchResultsPanel({ search, isLoading, isRunning, showPrompt = true, className }: Props) {
|
export function SearchResultsPanel({ search, isLoading, isRunning, showPrompt = true, className }: Props) {
|
||||||
|
const ANSWER_COLLAPSED_HEIGHT_CLASS = "h-[3rem]";
|
||||||
|
const [isAnswerExpanded, setIsAnswerExpanded] = useState(false);
|
||||||
|
const [canExpandAnswer, setCanExpandAnswer] = useState(false);
|
||||||
|
const answerBodyRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsAnswerExpanded(false);
|
||||||
|
setCanExpandAnswer(false);
|
||||||
|
}, [search?.id, search?.answerText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = answerBodyRef.current;
|
||||||
|
if (!el || !search?.answerText) {
|
||||||
|
setCanExpandAnswer(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isAnswerExpanded) {
|
||||||
|
setCanExpandAnswer(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCanExpandAnswer(el.scrollHeight - el.clientHeight > 1);
|
||||||
|
}, [search?.answerText, isAnswerExpanded]);
|
||||||
|
|
||||||
const citationEntries = (search?.answerCitations ?? [])
|
const citationEntries = (search?.answerCitations ?? [])
|
||||||
.map((citation, index) => {
|
.map((citation, index) => {
|
||||||
const href = citation.url || citation.id || "";
|
const href = citation.url || citation.id || "";
|
||||||
@@ -68,6 +93,11 @@ export function SearchResultsPanel({ search, isLoading, isRunning, showPrompt =
|
|||||||
return citationEntries.find((entry) => entry.normalizedHref === normalized)?.index;
|
return citationEntries.find((entry) => entry.normalizedHref === normalized)?.index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasAnswerText = !!search?.answerText;
|
||||||
|
const isAnswerLoading = isRunning && !hasAnswerText;
|
||||||
|
const hasCitations = citationEntries.length > 0;
|
||||||
|
const isExpandable = hasAnswerText && (canExpandAnswer || hasCitations);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className ?? "mx-auto w-full max-w-4xl"}>
|
<div className={className ?? "mx-auto w-full max-w-4xl"}>
|
||||||
{search?.query ? (
|
{search?.query ? (
|
||||||
@@ -84,17 +114,47 @@ export function SearchResultsPanel({ search, isLoading, isRunning, showPrompt =
|
|||||||
{(isRunning || !!search?.answerText || !!search?.answerError) && (
|
{(isRunning || !!search?.answerText || !!search?.answerError) && (
|
||||||
<section className="mb-6 rounded-xl border border-slate-600/60 bg-[#121a2e] p-4">
|
<section className="mb-6 rounded-xl border border-slate-600/60 bg-[#121a2e] p-4">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-sky-300/90">Answer</p>
|
<p className="text-xs font-semibold uppercase tracking-wide text-sky-300/90">Answer</p>
|
||||||
{isRunning && !search?.answerText ? <p className="mt-2 text-sm text-muted-foreground">Generating answer...</p> : null}
|
{(isAnswerLoading || hasAnswerText) ? (
|
||||||
{search?.answerText ? (
|
<div className="mt-2">
|
||||||
<MarkdownContent
|
<div className="relative">
|
||||||
markdown={search.answerText}
|
<div
|
||||||
mode="citationTokens"
|
ref={answerBodyRef}
|
||||||
resolveCitationIndex={resolveCitationIndex}
|
className={cn(
|
||||||
className="mt-2 text-sm leading-6 text-slate-100"
|
"overflow-hidden",
|
||||||
/>
|
!isAnswerExpanded && ANSWER_COLLAPSED_HEIGHT_CLASS
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isAnswerLoading ? (
|
||||||
|
<div className="text-sm text-muted-foreground">Generating answer...</div>
|
||||||
|
) : (
|
||||||
|
<MarkdownContent
|
||||||
|
markdown={search?.answerText ?? ""}
|
||||||
|
mode="citationTokens"
|
||||||
|
resolveCitationIndex={resolveCitationIndex}
|
||||||
|
className="text-sm leading-6 text-slate-100"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isAnswerExpanded && (isExpandable || isAnswerLoading) ? (
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-7 bg-gradient-to-t from-[#121a2e] to-transparent" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-5">
|
||||||
|
{isExpandable ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-1 text-xs font-medium text-sky-200 hover:text-sky-100"
|
||||||
|
onClick={() => setIsAnswerExpanded((current) => !current)}
|
||||||
|
>
|
||||||
|
<span className={cn("inline-block text-[11px] transition-transform", isAnswerExpanded && "rotate-180")}>▾</span>
|
||||||
|
{isAnswerExpanded ? "Show less" : "Show more"}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{search?.answerError ? <p className="mt-2 text-sm text-red-500">{search.answerError}</p> : null}
|
{search?.answerError ? <p className="mt-2 text-sm text-red-500">{search.answerError}</p> : null}
|
||||||
{!!citationEntries.length && (
|
{isAnswerExpanded && !!citationEntries.length && (
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
{citationEntries.slice(0, 8).map((citation) => {
|
{citationEntries.slice(0, 8).map((citation) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user