diff --git a/apps/web/src/features/conversation/chat.ts b/apps/web/src/features/conversation/chat.ts index 1c8240d..cc76bd7 100644 --- a/apps/web/src/features/conversation/chat.ts +++ b/apps/web/src/features/conversation/chat.ts @@ -90,6 +90,9 @@ export function useChat(skey: string, options: ChatOptions = {}) { return refresh(skey) }, }) + // The stable functions alone, so what calls them stays the same from render to render. + const { mutate: stop } = stopping + const { mutate: regenerate } = regenerating const leaf = branch.at(-1)?.message // The later of the two, so a later success clears an earlier failure. const outcome = [stopping, regenerating].reduce((a, b) => (b.submittedAt > a.submittedAt ? b : a)) @@ -114,10 +117,9 @@ export function useChat(skey: string, options: ChatOptions = {}) { running: leaf?.record.status === 'pending' ? leaf.rkey : null, send, /** Ask again for the reply to a user message, by its key, with a model or the same one. */ - regenerate: (parent: string | null, model: ModelRef | null) => - regenerating.mutate({ parent, model }), + regenerate: (parent: string | null, model: ModelRef | null) => regenerate({ parent, model }), regenerating: regenerating.isPending, - stop: (replyRkey: string) => stopping.mutate(replyRkey), + stop: (replyRkey: string) => stop(replyRkey), /** How the last regeneration or stop went, for the conversation's one alert. */ outcome: { error: outcome.error, submittedAt: outcome.submittedAt }, } diff --git a/apps/web/src/features/conversation/components/BranchView.tsx b/apps/web/src/features/conversation/components/BranchView.tsx index b109344..55c26b1 100644 --- a/apps/web/src/features/conversation/components/BranchView.tsx +++ b/apps/web/src/features/conversation/components/BranchView.tsx @@ -1,10 +1,10 @@ -import type { ReactNode } from 'react' +import { memo, type ReactNode } from 'react' import type { BlobUrl } from '../../../parts/types.ts' import type { BranchMessage, BranchStep } from '../lib/branch.ts' import { MessageView } from './MessageView.tsx' -interface Props { - branch: BranchStep[] +/** What the conversation gives every message on the branch. */ +interface RowOptions { blobUrl: BlobUrl /** Show another version of a message, by its rkey. */ onPick: (rkey: string) => void @@ -20,25 +20,71 @@ interface Props { arrived?: (message: BranchMessage) => boolean } +interface Props extends RowOptions { + branch: BranchStep[] +} + /** The messages on the branch on screen, each with its versions, and the id `m-`. */ export function BranchView(props: Props) { return ( <> - {props.branch.map(({ message, siblings, index }) => ( - ( + ))} ) } + +interface RowProps extends RowOptions { + message: BranchMessage + /** Its place among its versions, and how many there are. */ + index: number + count: number + /** The versions on either side, by rkey. */ + previous?: string + next?: string +} + +/** + * One message on the branch. It renders again only when its own record or its place among its + * versions changes, since the local copy keeps an unchanged message the same object, so a message + * arriving or a reply finishing doesn't render every other message again. + */ +const BranchRow = memo(function BranchRow(props: RowProps) { + const message = props.message + const status = message.record.status + return ( + + ) +}) diff --git a/apps/web/src/features/conversation/components/ConversationView.tsx b/apps/web/src/features/conversation/components/ConversationView.tsx index 3615421..7ccea77 100644 --- a/apps/web/src/features/conversation/components/ConversationView.tsx +++ b/apps/web/src/features/conversation/components/ConversationView.tsx @@ -19,7 +19,7 @@ import { ConversationHeader } from './ConversationHeader.tsx' import { ConversationSkeleton } from './ConversationSkeleton.tsx' import { EmptyConversation } from './EmptyConversation.tsx' import { LiveAuthor } from './LiveAuthor.tsx' -import { type ConversationOperations, MessageActions } from './message-actions.tsx' +import { type ConversationOperations, MessageActions, RunningReply } from './message-actions.tsx' import { ReplyStream } from './ReplyStream.tsx' interface Props { @@ -56,7 +56,6 @@ export function ConversationView(props: Props) { const operations: ConversationOperations = { edit: setEditingRkey, quote: (text) => composer.current?.quote(text), - running: stoppable, regenerate: chat.regenerate, stop: chat.stop, models, @@ -88,38 +87,40 @@ export function ConversationView(props: Props) { {waiting && } {chat.empty && composer.current?.fill(text)} />} - ( - - )} - live={(message) => ( - - )} - actions={(message) => } - // A failed reply tries again with the same model. - retry={(message) => ( - - )} - arrived={(message) => !opened?.has(message.rkey)} - /> + + ( + + )} + live={(message) => ( + + )} + actions={(message) => } + // A failed reply tries again with the same model. + retry={(message) => ( + + )} + arrived={(message) => !opened?.has(message.rkey)} + /> + - {props.siblings && props.siblings.messages.length > 1 && ( - - )} + {props.siblings && props.siblings.count > 1 && } {record.status === 'pending' && props.pending} diff --git a/apps/web/src/features/conversation/components/SiblingPicker.tsx b/apps/web/src/features/conversation/components/SiblingPicker.tsx index 70f6c4f..c64f340 100644 --- a/apps/web/src/features/conversation/components/SiblingPicker.tsx +++ b/apps/web/src/features/conversation/components/SiblingPicker.tsx @@ -1,10 +1,13 @@ import { ChevronLeftIcon, ChevronRightIcon } from '../../../shared/icons.tsx' -import type { BranchMessage } from '../lib/branch.ts' /** A message's alternative versions, and which one is shown. */ export interface Siblings { - messages: BranchMessage[] + /** The place of the one shown, and how many there are. */ index: number + count: number + /** The versions on either side, by rkey. */ + previous?: string + next?: string /** Show another version, by its rkey. */ onPick: (rkey: string) => void } @@ -19,27 +22,27 @@ const arrow = /** Arrows to step between alternative versions of a message, around a count that keeps its width. */ export function SiblingPicker(props: Props) { - const previous = props.siblings.messages[props.siblings.index - 1] - const next = props.siblings.messages[props.siblings.index + 1] + const previous = props.siblings.previous + const next = props.siblings.next return ( - {props.siblings.index + 1}/{props.siblings.messages.length} + {props.siblings.index + 1}/{props.siblings.count}