|
3 | 3 | * License: MS-RSL – see LICENSE.md for details
|
4 | 4 | */
|
5 | 5 |
|
6 |
| -import { List, Map, Seq, fromJS, Map as immutableMap } from "immutable"; |
| 6 | +import { List, Map, Seq, Map as immutableMap } from "immutable"; |
7 | 7 | import { debounce } from "lodash";
|
8 | 8 | import { Optional } from "utility-types";
|
9 | 9 | import { setDefaultLLM } from "@cocalc/frontend/account/useLanguageModelSetting";
|
@@ -44,14 +44,15 @@ import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
|
44 | 44 | import { getSortedDates, getUserName } from "./chat-log";
|
45 | 45 | import { message_to_markdown } from "./message";
|
46 | 46 | import { ChatState, ChatStore } from "./store";
|
47 |
| -import { |
| 47 | +import type { |
48 | 48 | ChatMessage,
|
49 | 49 | ChatMessageTyped,
|
50 |
| - ChatMessages, |
51 | 50 | Feedback,
|
52 | 51 | MessageHistory,
|
53 | 52 | } from "./types";
|
54 | 53 | import { history_path } from "@cocalc/util/misc";
|
| 54 | +import { initFromSyncDB, handleSyncDBChange, processSyncDBObj } from "./sync"; |
| 55 | +import { getReplyToRoot, getThreadRootDate } from "./utils"; |
55 | 56 |
|
56 | 57 | const MAX_CHATSTREAM = 10;
|
57 | 58 |
|
@@ -91,107 +92,19 @@ export class ChatActions extends Actions<ChatState> {
|
91 | 92 | delete this.syncdb;
|
92 | 93 | }
|
93 | 94 |
|
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 |
| - |
138 | 95 | // Initialize the state of the store from the contents of the syncdb.
|
139 | 96 | public init_from_syncdb(): void {
|
140 | 97 | if (this.syncdb == null) {
|
141 | 98 | return;
|
142 | 99 | }
|
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 }); |
154 | 101 | }
|
155 | 102 |
|
156 | 103 | 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 }); |
195 | 108 | }
|
196 | 109 |
|
197 | 110 | public foldThread(reply_to: Date, messageIndex?: number) {
|
@@ -312,6 +225,9 @@ export class ChatActions extends Actions<ChatState> {
|
312 | 225 |
|
313 | 226 | const project_id = this.store?.get("project_id");
|
314 | 227 | const path = this.store?.get("path");
|
| 228 | + if (!path) { |
| 229 | + throw Error("bug -- path must be defined"); |
| 230 | + } |
315 | 231 | // set notification saying that we sent an actual chat
|
316 | 232 | let action;
|
317 | 233 | if (
|
@@ -447,7 +363,10 @@ export class ChatActions extends Actions<ChatState> {
|
447 | 363 | const reply_to_value =
|
448 | 364 | reply_to != null
|
449 | 365 | ? 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 | + }); |
451 | 370 | const time_stamp_str = this.send_chat({
|
452 | 371 | input: reply,
|
453 | 372 | sender_id: from ?? this.redux.getStore("account").get_account_id(),
|
@@ -517,7 +436,7 @@ export class ChatActions extends Actions<ChatState> {
|
517 | 436 | const messages = this.store.get("messages");
|
518 | 437 | // message != null means this is a reply and we have to get the whole chat thread
|
519 | 438 | if (!model && message != null && messages != null) {
|
520 |
| - const root = getReplyToRoot(message, messages); |
| 439 | + const root = getReplyToRoot({ message, messages }); |
521 | 440 | model = this.isLanguageModelThread(root);
|
522 | 441 | if (!isFreeModel(model, is_cocalc_com) && root != null) {
|
523 | 442 | for (const msg of this.getLLMHistory(root)) {
|
@@ -871,10 +790,10 @@ export class ChatActions extends Actions<ChatState> {
|
871 | 790 | // if that happens, create a new message with the existing history and the new sender_id
|
872 | 791 | const cur = this.syncdb.get_one({ event: "chat", date });
|
873 | 792 | 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, |
876 | 795 | messages,
|
877 |
| - ); |
| 796 | + }); |
878 | 797 | this.syncdb.delete({ event: "chat", date });
|
879 | 798 | this.syncdb.set({
|
880 | 799 | date,
|
@@ -1078,8 +997,13 @@ export class ChatActions extends Actions<ChatState> {
|
1078 | 997 | if (this.syncdb == null) return;
|
1079 | 998 | const date = date0.toISOString();
|
1080 | 999 | 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 | + } |
1083 | 1007 | const reply_to = message.reply_to;
|
1084 | 1008 | if (!reply_to) return;
|
1085 | 1009 | await this.processLLM({
|
@@ -1114,31 +1038,6 @@ export class ChatActions extends Actions<ChatState> {
|
1114 | 1038 | };
|
1115 | 1039 | }
|
1116 | 1040 |
|
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 |
| - |
1142 | 1041 | // We strip out any cased version of the string @chatgpt and also all mentions.
|
1143 | 1042 | function stripMentions(value: string): string {
|
1144 | 1043 | for (const name of ["@chatgpt4", "@chatgpt"]) {
|
|
0 commit comments