Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/chat/side-chat.tsx
1496 views
1
import { Button, Flex, Space, Tooltip } from "antd";
2
import { useCallback, useEffect, useRef, useState } from "react";
3
import {
4
CSS,
5
redux,
6
useActions,
7
useRedux,
8
useTypedRedux,
9
} from "@cocalc/frontend/app-framework";
10
import { AddCollaborators } from "@cocalc/frontend/collaborators";
11
import { A, Icon, Loading } from "@cocalc/frontend/components";
12
import { IS_MOBILE } from "@cocalc/frontend/feature";
13
import { ProjectUsers } from "@cocalc/frontend/projects/project-users";
14
import { user_activity } from "@cocalc/frontend/tracker";
15
import { COLORS } from "@cocalc/util/theme";
16
import type { ChatActions } from "./actions";
17
import { ChatLog } from "./chat-log";
18
import Filter from "./filter";
19
import ChatInput from "./input";
20
import { LLMCostEstimationChat } from "./llm-cost-estimation";
21
import { SubmitMentionsFn } from "./types";
22
import { INPUT_HEIGHT, markChatAsReadIfUnseen } from "./utils";
23
24
interface Props {
25
project_id: string;
26
path: string;
27
style?: CSS;
28
fontSize?: number;
29
actions?: ChatActions;
30
desc?;
31
}
32
33
export default function SideChat({
34
actions: actions0,
35
project_id,
36
path,
37
style,
38
fontSize,
39
desc,
40
}: Props) {
41
// This actionsViaContext via useActions is ONLY needed for side chat for non-frame
42
// editors, i.e., basically just Sage Worksheets!
43
const actionsViaContext = useActions(project_id, path);
44
const actions: ChatActions = actions0 ?? actionsViaContext;
45
const disableFilters = actions0 == null;
46
const messages = useRedux(["messages"], project_id, path);
47
const [lastVisible, setLastVisible] = useState<Date | null>(null);
48
const [input, setInput] = useState("");
49
const search = desc?.get("data-search") ?? "";
50
const selectedHashtags = desc?.get("data-selectedHashtags");
51
const scrollToIndex = desc?.get("data-scrollToIndex") ?? null;
52
const scrollToDate = desc?.get("data-scrollToDate") ?? null;
53
const fragmentId = desc?.get("data-fragmentId") ?? null;
54
const costEstimate = desc?.get("data-costEstimate");
55
const addCollab: boolean = useRedux(["add_collab"], project_id, path);
56
const project_map = useTypedRedux("projects", "project_map");
57
const project = project_map?.get(project_id);
58
const scrollToBottomRef = useRef<any>(null);
59
const submitMentionsRef = useRef<SubmitMentionsFn | undefined>(undefined);
60
61
const markAsRead = useCallback(() => {
62
markChatAsReadIfUnseen(project_id, path);
63
}, [project_id, path]);
64
65
// The act of opening/displaying the chat marks it as seen...
66
// since this happens when the user shows it.
67
useEffect(() => {
68
markAsRead();
69
}, []);
70
71
const sendChat = useCallback(
72
(options?) => {
73
actions.sendChat({ submitMentionsRef, ...options });
74
actions.deleteDraft(0);
75
scrollToBottomRef.current?.(true);
76
setTimeout(() => {
77
scrollToBottomRef.current?.(true);
78
}, 10);
79
setTimeout(() => {
80
scrollToBottomRef.current?.(true);
81
}, 1000);
82
},
83
[actions],
84
);
85
86
if (messages == null) {
87
return <Loading />;
88
}
89
90
// WARNING: making autofocus true would interfere with chat and terminals
91
// -- where chat and terminal are both focused at same time sometimes
92
// (esp on firefox).
93
94
return (
95
<div
96
style={{
97
height: "100%",
98
width: "100%",
99
display: "flex",
100
flexDirection: "column",
101
backgroundColor: "#efefef",
102
...style,
103
}}
104
onMouseMove={markAsRead}
105
onFocus={() => {
106
// Remove any active key handler that is next to this side chat.
107
// E.g, this is critical for tasks lists...
108
redux.getActions("page").erase_active_key_handler();
109
}}
110
>
111
{!IS_MOBILE && project != null && actions != null && (
112
<div
113
style={{
114
margin: "0 5px",
115
paddingTop: "5px",
116
maxHeight: "50vh",
117
overflow: "auto",
118
borderBottom: "1px solid lightgrey",
119
}}
120
>
121
<CollabList
122
addCollab={addCollab}
123
project={project}
124
actions={actions}
125
/>
126
<AddChatCollab addCollab={addCollab} project_id={project_id} />
127
</div>
128
)}
129
{!disableFilters && (
130
<Filter
131
actions={actions}
132
search={search}
133
style={{
134
margin: 0,
135
...(messages.size >= 2
136
? undefined
137
: { visibility: "hidden", height: 0 }),
138
}}
139
/>
140
)}
141
<div
142
className="smc-vfill"
143
style={{
144
backgroundColor: "#fff",
145
paddingLeft: "15px",
146
flex: 1,
147
margin: "5px 0",
148
}}
149
>
150
<ChatLog
151
actions={actions}
152
fontSize={fontSize}
153
project_id={project_id}
154
path={path}
155
scrollToBottomRef={scrollToBottomRef}
156
mode={"sidechat"}
157
setLastVisible={setLastVisible}
158
search={search}
159
selectedHashtags={selectedHashtags}
160
disableFilters={disableFilters}
161
scrollToIndex={scrollToIndex}
162
scrollToDate={scrollToDate}
163
selectedDate={fragmentId}
164
costEstimate={costEstimate}
165
/>
166
</div>
167
168
<div>
169
{input.trim() ? (
170
<Flex
171
vertical={false}
172
align="center"
173
justify="space-between"
174
style={{ margin: "5px" }}
175
>
176
<Space>
177
{lastVisible && (
178
<Tooltip title="Reply to the current thread (shift+enter)">
179
<Button
180
disabled={!input.trim() || actions == null}
181
type="primary"
182
onClick={() => {
183
sendChat({ reply_to: new Date(lastVisible) });
184
}}
185
>
186
<Icon name="reply" /> Reply
187
</Button>
188
</Tooltip>
189
)}
190
<Tooltip
191
title={
192
lastVisible
193
? "Start a new thread"
194
: "Start a new thread (shift+enter)"
195
}
196
>
197
<Button
198
type={!lastVisible ? "primary" : undefined}
199
style={{ marginLeft: "5px" }}
200
onClick={() => {
201
sendChat();
202
user_activity("side_chat", "send_chat", "click");
203
}}
204
disabled={!input?.trim() || actions == null}
205
>
206
<Icon name="paper-plane" />
207
New Thread
208
</Button>
209
</Tooltip>
210
</Space>
211
<div style={{ flex: 1 }} />
212
<Space>
213
<Tooltip title={"Launch video chat specific to this document"}>
214
<Button
215
disabled={actions == null}
216
onClick={() => {
217
actions?.frameTreeActions?.getVideoChat().startChatting();
218
}}
219
>
220
<Icon name="video-camera" />
221
Video
222
</Button>
223
</Tooltip>
224
{costEstimate?.get("date") == 0 && (
225
<LLMCostEstimationChat
226
compact
227
costEstimate={costEstimate?.toJS()}
228
style={{ margin: "5px" }}
229
/>
230
)}
231
</Space>
232
</Flex>
233
) : undefined}
234
<ChatInput
235
autoFocus
236
fontSize={fontSize}
237
cacheId={`${path}${project_id}-new`}
238
input={input}
239
on_send={() => {
240
sendChat(lastVisible ? { reply_to: lastVisible } : undefined);
241
user_activity("side_chat", "send_chat", "keyboard");
242
actions?.clearAllFilters();
243
}}
244
style={{ height: INPUT_HEIGHT }}
245
height={INPUT_HEIGHT}
246
onChange={(value) => {
247
setInput(value);
248
// submitMentionsRef processes the reply, but does not actually send the mentions
249
const input = submitMentionsRef.current?.(undefined, true) ?? value;
250
actions?.llmEstimateCost({ date: 0, input });
251
}}
252
submitMentionsRef={submitMentionsRef}
253
syncdb={actions.syncdb}
254
date={0}
255
editBarStyle={{ overflow: "none" }}
256
/>
257
</div>
258
</div>
259
);
260
}
261
262
function AddChatCollab({ addCollab, project_id }) {
263
if (!addCollab) {
264
return null;
265
}
266
return (
267
<div>
268
@mention AI or collaborators, add more collaborators below, or{" "}
269
<A href="https://discord.gg/EugdaJZ8">join the CoCalc Discord.</A>
270
<AddCollaborators project_id={project_id} autoFocus where="side-chat" />
271
<div style={{ color: COLORS.GRAY_M }}>
272
(Collaborators have access to all files in this project.)
273
</div>
274
</div>
275
);
276
}
277
278
function CollabList({ project, addCollab, actions }) {
279
return (
280
<div
281
style={
282
!addCollab
283
? {
284
maxHeight: "1.7em",
285
whiteSpace: "nowrap",
286
overflow: "hidden",
287
textOverflow: "ellipsis",
288
cursor: "pointer",
289
}
290
: { cursor: "pointer" }
291
}
292
onClick={() => actions.setState({ add_collab: !addCollab })}
293
>
294
<div style={{ width: "16px", display: "inline-block" }}>
295
<Icon name={addCollab ? "caret-down" : "caret-right"} />
296
</div>
297
<span style={{ color: COLORS.GRAY_M, fontSize: "10pt" }}>
298
<ProjectUsers
299
project={project}
300
none={<span>Add people to work with...</span>}
301
/>
302
</span>
303
</div>
304
);
305
}
306
307