Download src/components/study-chat/PlainAssistantMessage.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 3.16 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/PlainAssistantMessage.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/study-chat/PlainAssistantMessage.tsx
-
curl -L -o PlainAssistantMessage.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/study-chat/PlainAssistantMessage.tsx
3.16 kB
| "use client"; | |
| import { useState } from "react"; | |
| import ReactMarkdown from "react-markdown"; | |
| import rehypeKatex from "rehype-katex"; | |
| import remarkGfm from "remark-gfm"; | |
| import remarkMath from "remark-math"; | |
| import { Copy } from "lucide-react"; | |
| import { markdownComponents } from "./StudyCardPrimitives"; | |
| import { cleanLegacySourceSafetyText } from "./utils"; | |
| // Markdown tokens would otherwise be read aloud as raw symbols ("hash", "asterisk"). | |
| function plainAnnouncementText(text: string) { | |
| return text | |
| .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1") | |
| .replace(/[*_`>#]+/g, " ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| } | |
| /** | |
| * A streamed answer grows one token at a time. Announcing that raw string would | |
| * make a screen reader repeat itself on every token, so we announce only the | |
| * sentences that are already complete. Each finished sentence becomes its own | |
| * node in the live region below, so assistive tech reads just the new sentence | |
| * and never a half-typed word. | |
| */ | |
| function settledSentences(text: string): string[] { | |
| const sentences: string[] = []; | |
| let start = 0; | |
| const boundary = /[.!?](?=\s|$)|\n{2,}/g; | |
| let match: RegExpExecArray | null; | |
| while ((match = boundary.exec(text)) !== null) { | |
| const end = match.index + match[0].length; | |
| const sentence = plainAnnouncementText(text.slice(start, end)); | |
| if (sentence) sentences.push(sentence); | |
| start = end; | |
| } | |
| return sentences; | |
| } | |
| export function PlainAssistantMessage({ text, streaming = false }: { text: string; streaming?: boolean }) { | |
| const [copied, setCopied] = useState(false); | |
| const displayText = cleanLegacySourceSafetyText(text); | |
| const announcement = streaming ? settledSentences(displayText) : []; | |
| async function copyText() { | |
| try { | |
| await navigator.clipboard.writeText(displayText); | |
| setCopied(true); | |
| window.setTimeout(() => setCopied(false), 1200); | |
| } catch { | |
| setCopied(false); | |
| } | |
| } | |
| return ( | |
| <div className="animate-msg-assistant max-w-[min(48rem,100%)] rounded-[1.25rem] border border-white/[0.085] bg-white/[0.035] px-4 py-3 text-sm leading-7 text-white/76 shadow-[0_14px_50px_rgba(0,0,0,0.2)]"> | |
| {streaming ? ( | |
| <div role="status" aria-live="polite" aria-atomic="false" className="sr-only"> | |
| {announcement.map((sentence, index) => ( | |
| <span key={index}>{sentence} </span> | |
| ))} | |
| </div> | |
| ) : null} | |
| <div className={streaming ? "kb-stream-cursor" : undefined}> | |
| <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]} components={markdownComponents}> | |
| {displayText || (streaming ? " " : "")} | |
| </ReactMarkdown> | |
| </div> | |
| {!streaming ? ( | |
| <button | |
| type="button" | |
| onClick={copyText} | |
| className="mt-3 inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] px-2.5 py-1 text-[11px] font-semibold text-white/44 transition hover:text-white/74 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20" | |
| > | |
| <Copy className="size-3" /> | |
| {copied ? "Copied" : "Copy"} | |
| </button> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |