Skip to content

Commit f4d98e1

Browse files
committed
chat: rewrite to be a normal frame editor -- work in progress that mostly works
1 parent 7628d28 commit f4d98e1

File tree

11 files changed

+296
-320
lines changed

11 files changed

+296
-320
lines changed

src/packages/frontend/chat/actions.ts

Lines changed: 27 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
import { List, Map, Seq, fromJS, Map as immutableMap } from "immutable";
6+
import { List, Map, Seq, Map as immutableMap } from "immutable";
77
import { debounce } from "lodash";
88
import { Optional } from "utility-types";
99
import { setDefaultLLM } from "@cocalc/frontend/account/useLanguageModelSetting";
@@ -44,14 +44,15 @@ import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
4444
import { getSortedDates, getUserName } from "./chat-log";
4545
import { message_to_markdown } from "./message";
4646
import { ChatState, ChatStore } from "./store";
47-
import {
47+
import type {
4848
ChatMessage,
4949
ChatMessageTyped,
50-
ChatMessages,
5150
Feedback,
5251
MessageHistory,
5352
} from "./types";
5453
import { history_path } from "@cocalc/util/misc";
54+
import { initFromSyncDB, handleSyncDBChange, processSyncDBObj } from "./sync";
55+
import { getReplyToRoot, getThreadRootDate } from "./utils";
5556

5657
const MAX_CHATSTREAM = 10;
5758

@@ -91,107 +92,19 @@ export class ChatActions extends Actions<ChatState> {
9192
delete this.syncdb;
9293
}
9394

94-
// NOTE: x must be already a plain JS object (.toJS())
95-
private process_syncdb_obj(x): ChatMessage | undefined {
96-
if (x.event !== "chat") {
97-
// Event used to be used for video chat, etc...; but we have a better approach now, so
98-
// all events we care about are chat.
99-
return;
100-
}
101-
if (x.video_chat != null ? x.video_chat.is_video_chat : undefined) {
102-
// discard/ignore anything else related to the old old video chat approach
103-
return;
104-
}
105-
x.date = new Date(x.date);
106-
if ((x.history != null ? x.history.length : undefined) > 0) {
107-
// nontrivial history -- nothing to do
108-
} else if (x.payload != null) {
109-
// for old chats with payload: content (2014-2016)... plus the script @hsy wrote in the work project ;-(
110-
x.history = [];
111-
x.history.push({
112-
content: x.payload.content,
113-
author_id: x.sender_id,
114-
date: new Date(x.date),
115-
});
116-
delete x.payload;
117-
} else if (x.mesg != null) {
118-
// for old chats with mesg: content (up to 2014)
119-
x.history = [];
120-
x.history.push({
121-
content: x.mesg.content,
122-
author_id: x.sender_id,
123-
date: new Date(x.date),
124-
});
125-
delete x.mesg;
126-
}
127-
if (x.history == null) {
128-
x.history = [];
129-
}
130-
if (!x.editing) {
131-
x.editing = {};
132-
}
133-
x.folding ??= [];
134-
x.feedback ??= {};
135-
return x;
136-
}
137-
13895
// Initialize the state of the store from the contents of the syncdb.
13996
public init_from_syncdb(): void {
14097
if (this.syncdb == null) {
14198
return;
14299
}
143-
const v = {};
144-
for (let x of this.syncdb.get().toJS()) {
145-
x = this.process_syncdb_obj(x);
146-
if (x != null) {
147-
v[x.date.valueOf()] = x;
148-
}
149-
}
150-
151-
this.setState({
152-
messages: fromJS(v) as any,
153-
});
100+
initFromSyncDB({ syncdb: this.syncdb, store: this.store });
154101
}
155102

156103
public syncdb_change(changes): void {
157-
changes.map((obj) => {
158-
if (this.syncdb == null) return;
159-
obj = obj.toJS();
160-
if (obj.event === "draft") {
161-
let drafts = this.store?.get("drafts") ?? (fromJS({}) as any);
162-
// used to show that another user is editing a message.
163-
const record = this.syncdb.get_one(obj);
164-
if (record == null) {
165-
drafts = drafts.delete(obj.sender_id);
166-
} else {
167-
const sender_id = record.get("sender_id");
168-
drafts = drafts.set(sender_id, record);
169-
}
170-
this.setState({ drafts });
171-
return;
172-
}
173-
if (obj.event === "chat") {
174-
let changed: boolean = false;
175-
let messages = this.store?.get("messages") ?? (fromJS({}) as any);
176-
obj.date = new Date(obj.date);
177-
const record = this.syncdb.get_one(obj);
178-
let x: any = record?.toJS();
179-
if (x == null) {
180-
// delete
181-
messages = messages.delete(`${obj.date.valueOf()}`);
182-
changed = true;
183-
} else {
184-
x = this.process_syncdb_obj(x);
185-
if (x != null) {
186-
messages = messages.set(`${x.date.valueOf()}`, fromJS(x));
187-
changed = true;
188-
}
189-
}
190-
if (changed) {
191-
this.setState({ messages });
192-
}
193-
}
194-
});
104+
if (this.syncdb == null) {
105+
return;
106+
}
107+
handleSyncDBChange({ changes, store: this.store, syncdb: this.syncdb });
195108
}
196109

197110
public foldThread(reply_to: Date, messageIndex?: number) {
@@ -312,6 +225,9 @@ export class ChatActions extends Actions<ChatState> {
312225

313226
const project_id = this.store?.get("project_id");
314227
const path = this.store?.get("path");
228+
if (!path) {
229+
throw Error("bug -- path must be defined");
230+
}
315231
// set notification saying that we sent an actual chat
316232
let action;
317233
if (
@@ -447,7 +363,10 @@ export class ChatActions extends Actions<ChatState> {
447363
const reply_to_value =
448364
reply_to != null
449365
? reply_to.valueOf()
450-
: store.getThreadRootDate(new Date(message.date).valueOf());
366+
: getThreadRootDate({
367+
date: new Date(message.date).valueOf(),
368+
messages: store.get("messages"),
369+
});
451370
const time_stamp_str = this.send_chat({
452371
input: reply,
453372
sender_id: from ?? this.redux.getStore("account").get_account_id(),
@@ -517,7 +436,7 @@ export class ChatActions extends Actions<ChatState> {
517436
const messages = this.store.get("messages");
518437
// message != null means this is a reply and we have to get the whole chat thread
519438
if (!model && message != null && messages != null) {
520-
const root = getReplyToRoot(message, messages);
439+
const root = getReplyToRoot({ message, messages });
521440
model = this.isLanguageModelThread(root);
522441
if (!isFreeModel(model, is_cocalc_com) && root != null) {
523442
for (const msg of this.getLLMHistory(root)) {
@@ -871,10 +790,10 @@ export class ChatActions extends Actions<ChatState> {
871790
// if that happens, create a new message with the existing history and the new sender_id
872791
const cur = this.syncdb.get_one({ event: "chat", date });
873792
if (cur == null) return;
874-
const reply_to = getReplyToRoot(
875-
cur.toJS() as any as ChatMessage,
793+
const reply_to = getReplyToRoot({
794+
message: cur.toJS() as any as ChatMessage,
876795
messages,
877-
);
796+
});
878797
this.syncdb.delete({ event: "chat", date });
879798
this.syncdb.set({
880799
date,
@@ -1078,8 +997,13 @@ export class ChatActions extends Actions<ChatState> {
1078997
if (this.syncdb == null) return;
1079998
const date = date0.toISOString();
1080999
const obj = this.syncdb.get_one({ event: "chat", date });
1081-
const message = this.process_syncdb_obj(obj?.toJS());
1082-
if (message == null) return;
1000+
if (obj == null) {
1001+
return;
1002+
}
1003+
const message = processSyncDBObj(obj.toJS() as ChatMessage);
1004+
if (message == null) {
1005+
return;
1006+
}
10831007
const reply_to = message.reply_to;
10841008
if (!reply_to) return;
10851009
await this.processLLM({
@@ -1114,31 +1038,6 @@ export class ChatActions extends Actions<ChatState> {
11141038
};
11151039
}
11161040

1117-
export function getRootMessage(
1118-
message: ChatMessage,
1119-
messages: ChatMessages,
1120-
): ChatMessageTyped | undefined {
1121-
const { reply_to, date } = message;
1122-
// we can't find the original message, if there is no reply_to
1123-
if (!reply_to) {
1124-
// the msssage itself is the root
1125-
return messages.get(`${new Date(date).valueOf()}`);
1126-
} else {
1127-
// All messages in a thread have the same reply_to, which points to the root.
1128-
return messages.get(`${new Date(reply_to).valueOf()}`);
1129-
}
1130-
}
1131-
1132-
export function getReplyToRoot(
1133-
message: ChatMessage,
1134-
messages: ChatMessages,
1135-
): Date | undefined {
1136-
const root = getRootMessage(message, messages);
1137-
const date = root?.get("date");
1138-
// date is a "Date" object, but we're just double checking it is not a string by accident
1139-
return date ? new Date(date) : undefined;
1140-
}
1141-
11421041
// We strip out any cased version of the string @chatgpt and also all mentions.
11431042
function stripMentions(value: string): string {
11441043
for (const name of ["@chatgpt4", "@chatgpt"]) {

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ import { Set as immutableSet } from "immutable";
1212
import { MutableRefObject, useEffect, useMemo, useRef } from "react";
1313
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
1414
import { chatBotName, isChatBot } from "@cocalc/frontend/account/chatbot";
15-
import {
16-
useActions,
17-
useRedux,
18-
useTypedRedux,
19-
} from "@cocalc/frontend/app-framework";
15+
import { useRedux, useTypedRedux } from "@cocalc/frontend/app-framework";
2016
import { Icon } from "@cocalc/frontend/components";
2117
import useVirtuosoScrollHook from "@cocalc/frontend/components/virtuoso-scroll-hook";
2218
import { HashtagBar } from "@cocalc/frontend/editors/task-editor/hashtag-bar";
@@ -26,11 +22,12 @@ import {
2622
parse_hashtags,
2723
plural,
2824
} from "@cocalc/util/misc";
29-
import { ChatActions, getRootMessage } from "./actions";
25+
import type { ChatActions } from "./actions";
3026
import Composing from "./composing";
3127
import Message from "./message";
3228
import type { ChatMessageTyped, ChatMessages, Mode } from "./types";
3329
import { getSelectedHashtagsSearch, newest_content } from "./utils";
30+
import { getRootMessage } from "./utils";
3431
import { DivTempHeight } from "@cocalc/frontend/jupyter/cell-list";
3532
import { filterMessages } from "./filter-messages";
3633

@@ -41,6 +38,7 @@ interface Props {
4138
scrollToBottomRef?: MutableRefObject<(force?: boolean) => void>;
4239
setLastVisible?: (x: Date | null) => void;
4340
fontSize?: number;
41+
actions: ChatActions;
4442
}
4543

4644
export function ChatLog({
@@ -50,8 +48,8 @@ export function ChatLog({
5048
mode,
5149
setLastVisible,
5250
fontSize,
51+
actions,
5352
}: Props) {
54-
const actions: ChatActions = useActions(project_id, path);
5553
const messages = useRedux(["messages"], project_id, path) as ChatMessages;
5654
const font_size = useRedux(["font_size"], project_id, path);
5755
const scrollToBottom = useRedux(["scrollToBottom"], project_id, path);
@@ -253,7 +251,7 @@ function isFolded(
253251
account_id?: string,
254252
) {
255253
if (account_id == null) return false;
256-
const rootMsg = getRootMessage(message.toJS(), messages);
254+
const rootMsg = getRootMessage({ message: message.toJS(), messages });
257255
return rootMsg?.get("folding")?.includes(account_id) ?? false;
258256
}
259257

@@ -459,9 +457,15 @@ export function MessageList({
459457
const h = virtuosoHeightsRef.current[index];
460458

461459
return (
462-
<div style={{ overflow: "hidden" }}>
460+
<div
461+
style={{
462+
overflow: "hidden",
463+
paddingTop: index == 0 ? "20px" : undefined,
464+
}}
465+
>
463466
<DivTempHeight height={h ? `${h}px` : undefined}>
464467
<Message
468+
messages={messages}
465469
key={date}
466470
index={index}
467471
account_id={account_id}

0 commit comments

Comments
 (0)