Skip to content

Commit f004bf7

Browse files
committed
fix #773 -- implement chat TimeTravel output rendering properly
- I just did this mainly as an excuse to slightly improve the code quality
1 parent 081fdc4 commit f004bf7

File tree

8 files changed

+253
-119
lines changed

8 files changed

+253
-119
lines changed

src/packages/frontend/chat/actions.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import { List, Map, Seq, fromJS, Map as immutableMap } from "immutable";
77
import { debounce } from "lodash";
88
import { Optional } from "utility-types";
9-
109
import { setDefaultLLM } from "@cocalc/frontend/account/useLanguageModelSetting";
1110
import { Actions, redux } from "@cocalc/frontend/app-framework";
1211
import { History as LanguageModelHistory } from "@cocalc/frontend/client/types";
@@ -53,6 +52,7 @@ import {
5352
MessageHistory,
5453
} from "./types";
5554
import { getSelectedHashtagsSearch } from "./utils";
55+
import { history_path } from "@cocalc/util/misc";
5656

5757
const MAX_CHATSTREAM = 10;
5858

@@ -1122,6 +1122,16 @@ export class ChatActions extends Actions<ChatState> {
11221122
setDefaultLLM(llm);
11231123
}
11241124
}
1125+
1126+
showTimeTravelInNewTab = () => {
1127+
const store = this.store;
1128+
if (store == null) return;
1129+
redux.getProjectActions(store.get("project_id")!).open_file({
1130+
path: history_path(store.get("path")!),
1131+
foreground: true,
1132+
foreground_project: true,
1133+
});
1134+
};
11251135
}
11261136

11271137
export function getRootMessage(

src/packages/frontend/chat/chat-log.tsx

Lines changed: 146 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { Alert } from "antd";
1111
import { List, Set as immutableSet } from "immutable";
1212
import { MutableRefObject, useEffect, useMemo, useRef } from "react";
1313
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
14-
1514
import { chatBotName, isChatBot } from "@cocalc/frontend/account/chatbot";
1615
import {
1716
TypedMap,
@@ -55,7 +54,6 @@ export function ChatLog(props: Readonly<Props>) {
5554
project_id,
5655
path,
5756
);
58-
const virtuosoHeightsRef = useRef<{ [index: number]: number }>({});
5957

6058
// see similar code in task list:
6159
const selectedHashtags0 = useRedux(["selectedHashtags"], project_id, path);
@@ -132,11 +130,6 @@ export function ChatLog(props: Readonly<Props>) {
132130
};
133131
}, [scrollToBottomRef != null]);
134132

135-
const virtuosoScroll = useVirtuosoScrollHook({
136-
cacheId: `${project_id}${path}`,
137-
initialState: { index: messages.size - 1, offset: 0 }, // starts scrolled to the newest message.
138-
});
139-
140133
return (
141134
<>
142135
{visibleHashtags.size > 0 && (
@@ -159,90 +152,23 @@ export function ChatLog(props: Readonly<Props>) {
159152
filterRecentH={filterRecentH}
160153
/>
161154
)}
162-
<Virtuoso
163-
ref={virtuosoRef}
164-
totalCount={sortedDates.length}
165-
itemSize={(el) => {
166-
// see comment in jupyter/cell-list.tsx
167-
const h = el.getBoundingClientRect().height;
168-
const data = el.getAttribute("data-item-index");
169-
if (data != null) {
170-
const index = parseInt(data);
171-
virtuosoHeightsRef.current[index] = h;
172-
}
173-
return h;
174-
}}
175-
itemContent={(index) => {
176-
const date = sortedDates[index];
177-
const message: ChatMessageTyped | undefined = messages.get(date);
178-
if (message == null) {
179-
// shouldn't happen. But we should be robust to such a possibility.
180-
return <div style={{ height: "1px" }} />;
181-
}
182-
183-
const is_thread = isThread(messages, message);
184-
// if we search for a message, we treat all threads as unfolded
185-
const force_unfold = !!search;
186-
const is_folded =
187-
!force_unfold && isFolded(messages, message, account_id);
188-
const is_thread_body = message.get("reply_to") != null;
189-
const h = virtuosoHeightsRef.current[index];
190-
191-
return (
192-
<div style={{ overflow: "hidden" }}>
193-
<DivTempHeight height={h ? `${h}px` : undefined}>
194-
<Message
195-
key={date}
196-
index={index}
197-
account_id={account_id}
198-
user_map={user_map}
199-
message={message}
200-
project_id={project_id}
201-
path={path}
202-
font_size={fontSize}
203-
selectedHashtags={selectedHashtags}
204-
actions={actions}
205-
is_thread={is_thread}
206-
is_folded={is_folded}
207-
force_unfold={force_unfold}
208-
is_thread_body={is_thread_body}
209-
is_prev_sender={isPrevMessageSender(
210-
index,
211-
sortedDates,
212-
messages,
213-
)}
214-
is_next_sender={isNextMessageSender(
215-
index,
216-
sortedDates,
217-
messages,
218-
)}
219-
show_avatar={
220-
!isNextMessageSender(index, sortedDates, messages)
221-
}
222-
mode={mode}
223-
get_user_name={(account_id: string | undefined) =>
224-
// ATTN: this also works for LLM chat bot IDs, not just account UUIDs
225-
typeof account_id === "string"
226-
? getUserName(user_map, account_id)
227-
: "Unknown name"
228-
}
229-
scroll_into_view={() =>
230-
virtuosoRef.current?.scrollIntoView({ index })
231-
}
232-
allowReply={
233-
messages.getIn([sortedDates[index + 1], "reply_to"]) == null
234-
}
235-
llm_cost_reply={llm_cost_reply}
236-
/>
237-
</DivTempHeight>
238-
</div>
239-
);
240-
}}
241-
rangeChanged={({ endIndex }) => {
242-
// manually scrolling if NOT at the bottom.
243-
manualScrollRef.current = endIndex < sortedDates.length - 1;
155+
<MessageList
156+
{...{
157+
virtuosoRef,
158+
sortedDates,
159+
messages,
160+
search,
161+
account_id,
162+
user_map,
163+
project_id,
164+
path,
165+
fontSize,
166+
selectedHashtags,
167+
actions,
168+
llm_cost_reply,
169+
manualScrollRef,
170+
mode,
244171
}}
245-
{...virtuosoScroll}
246172
/>
247173
<Composing
248174
projectId={project_id}
@@ -455,3 +381,133 @@ function NotShowing({ num, search, filterRecentH }: NotShowingProps) {
455381
/>
456382
);
457383
}
384+
385+
export function MessageList({
386+
messages,
387+
account_id,
388+
virtuosoRef,
389+
sortedDates,
390+
search,
391+
user_map,
392+
project_id,
393+
path,
394+
fontSize,
395+
selectedHashtags,
396+
actions,
397+
llm_cost_reply,
398+
manualScrollRef,
399+
mode,
400+
}: {
401+
messages;
402+
account_id;
403+
user_map;
404+
mode;
405+
sortedDates;
406+
virtuosoRef?;
407+
search?;
408+
project_id?;
409+
path?;
410+
fontSize?;
411+
selectedHashtags?;
412+
actions?;
413+
llm_cost_reply?;
414+
manualScrollRef?;
415+
}) {
416+
const virtuosoHeightsRef = useRef<{ [index: number]: number }>({});
417+
const virtuosoScroll = useVirtuosoScrollHook({
418+
cacheId: `${project_id}${path}`,
419+
initialState: { index: messages.size - 1, offset: 0 }, // starts scrolled to the newest message.
420+
});
421+
422+
return (
423+
<Virtuoso
424+
ref={virtuosoRef}
425+
totalCount={sortedDates.length}
426+
itemSize={(el) => {
427+
// see comment in jupyter/cell-list.tsx
428+
const h = el.getBoundingClientRect().height;
429+
const data = el.getAttribute("data-item-index");
430+
if (data != null) {
431+
const index = parseInt(data);
432+
virtuosoHeightsRef.current[index] = h;
433+
}
434+
return h;
435+
}}
436+
itemContent={(index) => {
437+
const date = sortedDates[index];
438+
const message: ChatMessageTyped | undefined = messages.get(date);
439+
if (message == null) {
440+
// shouldn't happen. But we should be robust to such a possibility.
441+
return <div style={{ height: "1px" }} />;
442+
}
443+
444+
const is_thread = isThread(messages, message);
445+
// if we search for a message, we treat all threads as unfolded
446+
const force_unfold = !!search;
447+
const is_folded =
448+
!force_unfold && isFolded(messages, message, account_id);
449+
const is_thread_body = message.get("reply_to") != null;
450+
const h = virtuosoHeightsRef.current[index];
451+
452+
return (
453+
<div style={{ overflow: "hidden" }}>
454+
<DivTempHeight height={h ? `${h}px` : undefined}>
455+
<Message
456+
key={date}
457+
index={index}
458+
account_id={account_id}
459+
user_map={user_map}
460+
message={message}
461+
project_id={project_id}
462+
path={path}
463+
font_size={fontSize}
464+
selectedHashtags={selectedHashtags}
465+
actions={actions}
466+
is_thread={is_thread}
467+
is_folded={is_folded}
468+
force_unfold={force_unfold}
469+
is_thread_body={is_thread_body}
470+
is_prev_sender={isPrevMessageSender(
471+
index,
472+
sortedDates,
473+
messages,
474+
)}
475+
is_next_sender={isNextMessageSender(
476+
index,
477+
sortedDates,
478+
messages,
479+
)}
480+
show_avatar={!isNextMessageSender(index, sortedDates, messages)}
481+
mode={mode}
482+
get_user_name={(account_id: string | undefined) =>
483+
// ATTN: this also works for LLM chat bot IDs, not just account UUIDs
484+
typeof account_id === "string"
485+
? getUserName(user_map, account_id)
486+
: "Unknown name"
487+
}
488+
scroll_into_view={
489+
virtuosoRef
490+
? () => virtuosoRef.current?.scrollIntoView({ index })
491+
: undefined
492+
}
493+
allowReply={
494+
messages.getIn([sortedDates[index + 1], "reply_to"]) == null
495+
}
496+
llm_cost_reply={llm_cost_reply}
497+
/>
498+
</DivTempHeight>
499+
</div>
500+
);
501+
}}
502+
rangeChanged={
503+
manualScrollRef
504+
? ({ endIndex }) => {
505+
// manually scrolling if NOT at the bottom.
506+
manualScrollRef.current = endIndex < sortedDates.length - 1;
507+
}
508+
: undefined
509+
}
510+
{...virtuosoScroll}
511+
/>
512+
);
513+
}

src/packages/frontend/chat/chatroom.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import SelectComputeServerForFile from "@cocalc/frontend/compute/select-server-f
3535
import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";
3636
import { SaveButton } from "@cocalc/frontend/frame-editors/frame-tree/save-button";
3737
import { sanitize_html_safe } from "@cocalc/frontend/misc";
38-
import { history_path, hoursToTimeIntervalHuman } from "@cocalc/util/misc";
38+
import { hoursToTimeIntervalHuman } from "@cocalc/util/misc";
3939
import { ChatActions } from "./actions";
4040
import { ChatLog } from "./chat-log";
4141
import ChatInput from "./input";
@@ -131,11 +131,7 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path, is_visible }) => {
131131
}
132132

133133
function show_timetravel(): void {
134-
redux.getProjectActions(project_id).open_file({
135-
path: history_path(path),
136-
foreground: true,
137-
foreground_project: true,
138-
});
134+
actions.showTimeTravelInNewTab();
139135
}
140136

141137
function render_preview_message(): JSX.Element | undefined {
@@ -558,12 +554,14 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path, is_visible }) => {
558554
// https://github.com/sagemathinc/cocalc/issues/7554
559555
return (
560556
<FrameContext.Provider
561-
value={{
562-
project_id,
563-
path,
564-
isVisible: !!is_visible,
565-
redux,
566-
} as any}
557+
value={
558+
{
559+
project_id,
560+
path,
561+
isVisible: !!is_visible,
562+
redux,
563+
} as any
564+
}
567565
>
568566
<div
569567
onMouseMove={mark_as_read}

src/packages/frontend/chat/message.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ interface Props {
112112
mode: Mode;
113113
selectedHashtags?: Set<string>;
114114

115-
scroll_into_view: () => void; // call to scroll this message into view
115+
scroll_into_view?: () => void; // call to scroll this message into view
116116

117117
// if true, include a reply button - this should only be for messages
118118
// that don't have an existing reply to them already.
@@ -232,7 +232,7 @@ export default function Message(props: Readonly<Props>) {
232232

233233
useLayoutEffect(() => {
234234
if (replying) {
235-
props.scroll_into_view();
235+
props.scroll_into_view?.();
236236
}
237237
}, [replying]);
238238

@@ -337,7 +337,7 @@ export default function Message(props: Readonly<Props>) {
337337
}
338338
props.actions.set_editing(message, true);
339339
setAutoFocusEdit(true);
340-
props.scroll_into_view();
340+
props.scroll_into_view?.();
341341
}
342342

343343
function avatar_column() {
@@ -736,7 +736,14 @@ export default function Message(props: Readonly<Props>) {
736736
}
737737

738738
function renderReplyRow() {
739-
if (replying || generating || !props.allowReply || is_folded) return;
739+
if (
740+
replying ||
741+
generating ||
742+
!props.allowReply ||
743+
is_folded ||
744+
props.actions == null
745+
)
746+
return;
740747

741748
return (
742749
<div style={{ textAlign: "center", marginBottom: "5px", width: "100%" }}>

0 commit comments

Comments
 (0)