Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260import React, { useState } from "react";import { ArrowClockwise, ArrowSquareOut, Check, ChatCircle, CopySimple, Trash, Sparkle, User, NotePencil,} from "@phosphor-icons/react";import { IconButton, Empty, Spinner } from "./ui.jsx";import { Markdown } from "./Markdown.jsx";import { messageSources } from "./messageSources.js";
export function AssistantTranscript({ loading, messages, material, busy, selectedChat, errors, transcriptRef, nearBottom, below, setBelow, onRemoveMessage, onRetry, onPage, notify, onSaveNote, onComparisonPage,}) { const [copiedMessageId, setCopiedMessageId] = useState(null); const retryableQuestion = [...messages] .reverse() .find((message) => message.role === "user"); const copyMessage = async (message) => { try { await navigator.clipboard.writeText(message.content); setCopiedMessageId(message.id); window.setTimeout( () => setCopiedMessageId((current) => current === message.id ? null : current, ), 1500, ); } catch { notify("Clipboard access was denied.", "error"); } };
return ( <> <div className="transcript" ref={transcriptRef} onScroll={() => { const element = transcriptRef.current; nearBottom.current = element.scrollHeight - element.scrollTop - element.clientHeight < 100; if (nearBottom.current) setBelow(false); }} aria-label="Conversation messages" aria-live="polite" aria-relevant="additions" > {loading ? ( <Spinner label="Loading conversation…" /> ) : !messages.length ? ( <Empty icon={ChatCircle} title="Ask anything about this material"> Select a passage or ask a question about this{" "} {material?.kind === "book" ? "book" : "article"}. </Empty> ) : ( messages.map((message, index) => ( <div key={message.id} data-message-id={message.id} className={"message " + message.role} > <div className="message-author"> {message.role === "user" ? ( <User size={16} /> ) : ( <Sparkle size={16} /> )} <span> {message.role === "user" ? "You" : message.model || "Assistant"} </span> </div> <IconButton label="Delete message" className="message-delete" disabled={!!busy[selectedChat]} onClick={() => onRemoveMessage(message)} > <Trash size={14} /> </IconButton> {message.role === "user" && message.context?.selection && ( <details className="message-context"> <summary> Selected passage {message.context.page ? " · page " + message.context.page : ""} </summary> <blockquote>{message.context.selection}</blockquote> </details> )} {message.content ? ( <Markdown content={message.content} materialId={material?.id} compact onPage={onPage} sources={messageSources(message, material?.sources)} comparisonCitations={message.citations} onComparisonPage={(page) => onComparisonPage( message.citations.find( (c) => c.comparison && c.page === page, )?.materialId, page, ) } /> ) : message.status === "streaming" ? ( <span className="thinking" role="status"> Thinking<span>…</span> </span> ) : ( <span className="muted">No answer was saved.</span> )} {message.role === "assistant" && ( <div className="message-footer"> {message.content && message.status === "complete" && ( <button className="text-button" onClick={() => onSaveNote(message, selectedChat)} > <NotePencil size={14} /> Save as note </button> )} {message.status !== "complete" && message.status !== "streaming" && ( <span>{message.status}</span> )} {message.content && ( <button type="button" className={ "copy-action " + (copiedMessageId === message.id ? "is-copied" : "") } aria-label={ copiedMessageId === message.id ? "Answer copied" : "Copy answer" } onClick={() => copyMessage(message)} > {copiedMessageId === message.id ? ( <Check size={14} weight="bold" /> ) : ( <CopySimple size={14} /> )} <span aria-live="polite"> {copiedMessageId === message.id ? "Copied" : "Copy"} </span> </button> )} {!busy[selectedChat] && index === messages.length - 1 && retryableQuestion && ( <button className="text-button" onClick={() => onRetry(retryableQuestion)} > <ArrowClockwise size={13} /> Retry </button> )} </div> )} {message.role === "assistant" && message.citations?.length > 0 && ( <details className="context-used"> <summary> Context used {message.context?.estimatedTokens ? " · ~" + message.context.estimatedTokens.toLocaleString() + " tokens" : ""} </summary> <div> {message.citations.map((citation, citationIndex) => citation.page ? ( <button className="citation" key={citationIndex} onClick={() => citation.comparison ? onComparisonPage( citation.materialId, citation.page, ) : onPage(citation.page) } > {citation.label} </button> ) : ( <a key={citationIndex} href={citation.url} target="_blank" rel="noreferrer" > {citation.label} <ArrowSquareOut size={12} /> </a> ), )} </div> </details> )} </div> )) )} </div> {below && ( <button className="new-reply" onClick={() => { nearBottom.current = true; transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight; setBelow(false); }} > Jump to latest reply </button> )} {errors[selectedChat] && ( <div className="chat-error" role="alert"> {errors[selectedChat]} </div> )} </> );}