CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/chat/side-chat.tsx
Views: 687
1
import { Button, Flex, Space, Tooltip } from "antd";
2
import { CSSProperties, useCallback, useEffect, useRef, useState } from "react";
3
import {
4
redux,
5
useActions,
6
useRedux,
7
useTypedRedux,
8
} from "@cocalc/frontend/app-framework";
9
import { AddCollaborators } from "@cocalc/frontend/collaborators";
10
import { A, Icon, Loading } from "@cocalc/frontend/components";
11
import { IS_MOBILE } from "@cocalc/frontend/feature";
12
import { ProjectUsers } from "@cocalc/frontend/projects/project-users";
13
import { user_activity } from "@cocalc/frontend/tracker";
14
import type { ChatActions } from "./actions";
15
import { ChatLog } from "./chat-log";
16
import ChatInput from "./input";
17
import { LLMCostEstimationChat } from "./llm-cost-estimation";
18
import { SubmitMentionsFn } from "./types";
19
import { INPUT_HEIGHT, markChatAsReadIfUnseen } from "./utils";
20
import VideoChatButton from "./video/launch-button";
21
import { COLORS } from "@cocalc/util/theme";
22
import Filter from "./filter";
23
24
interface Props {
25
project_id: string;
26
path: string;
27
style?: CSSProperties;
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>();
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 taks 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
<Space.Compact
122
style={{
123
float: "right",
124
marginTop: "-5px",
125
}}
126
>
127
<VideoChatButton actions={actions} />
128
<Tooltip title="Show TimeTravel change history of this side chat.">
129
<Button
130
onClick={() => {
131
actions.showTimeTravelInNewTab();
132
}}
133
>
134
<Icon name="history" />
135
</Button>
136
</Tooltip>
137
</Space.Compact>
138
<CollabList
139
addCollab={addCollab}
140
project={project}
141
actions={actions}
142
/>
143
<AddChatCollab addCollab={addCollab} project_id={project_id} />
144
</div>
145
)}
146
{!disableFilters && (
147
<Filter
148
actions={actions}
149
search={search}
150
style={{
151
margin: 0,
152
...(messages.size >= 2
153
? undefined
154
: { visibility: "hidden", height: 0 }),
155
}}
156
/>
157
)}
158
<div
159
className="smc-vfill"
160
style={{
161
backgroundColor: "#fff",
162
paddingLeft: "15px",
163
flex: 1,
164
margin: "5px 0",
165
}}
166
>
167
<ChatLog
168
actions={actions}
169
fontSize={fontSize}
170
project_id={project_id}
171
path={path}
172
scrollToBottomRef={scrollToBottomRef}
173
mode={"sidechat"}
174
setLastVisible={setLastVisible}
175
search={search}
176
selectedHashtags={selectedHashtags}
177
disableFilters={disableFilters}
178
scrollToIndex={scrollToIndex}
179
scrollToDate={scrollToDate}
180
selectedDate={fragmentId}
181
costEstimate={costEstimate}
182
/>
183
</div>
184
185
<div>
186
{input.trim() ? (
187
<Flex vertical={false} align="center" justify="space-between">
188
<Tooltip title="Send message (shift+enter)">
189
<Space>
190
{lastVisible && (
191
<Button
192
disabled={!input.trim()}
193
type="primary"
194
onClick={() => {
195
sendChat({ reply_to: new Date(lastVisible) });
196
}}
197
>
198
<Icon name="reply" /> Reply (shift+enter)
199
</Button>
200
)}
201
<Button
202
type={!lastVisible ? "primary" : undefined}
203
style={{ margin: "5px 0 5px 5px" }}
204
onClick={() => {
205
sendChat();
206
user_activity("side_chat", "send_chat", "click");
207
}}
208
disabled={!input?.trim()}
209
>
210
<Icon name="paper-plane" />
211
Start New Thread
212
</Button>
213
</Space>
214
</Tooltip>
215
{costEstimate?.get("date") == 0 && (
216
<LLMCostEstimationChat
217
compact
218
costEstimate={costEstimate?.toJS()}
219
style={{ margin: "5px" }}
220
/>
221
)}
222
</Flex>
223
) : undefined}
224
<ChatInput
225
autoFocus
226
fontSize={fontSize}
227
cacheId={`${path}${project_id}-new`}
228
input={input}
229
on_send={() => {
230
sendChat(lastVisible ? { reply_to: lastVisible } : undefined);
231
user_activity("side_chat", "send_chat", "keyboard");
232
actions?.clearAllFilters();
233
}}
234
style={{ height: INPUT_HEIGHT }}
235
height={INPUT_HEIGHT}
236
onChange={(value) => {
237
setInput(value);
238
// submitMentionsRef processes the reply, but does not actually send the mentions
239
const input = submitMentionsRef.current?.(undefined, true) ?? value;
240
actions?.llmEstimateCost({ date: 0, input });
241
}}
242
submitMentionsRef={submitMentionsRef}
243
syncdb={actions.syncdb}
244
date={0}
245
editBarStyle={{ overflow: "none" }}
246
/>
247
</div>
248
</div>
249
);
250
}
251
252
function AddChatCollab({ addCollab, project_id }) {
253
if (!addCollab) {
254
return null;
255
}
256
return (
257
<div>
258
You can{" "}
259
{redux.getProjectsStore().hasLanguageModelEnabled(project_id) && (
260
<>chat with AI or notify a collaborator by typing @, </>
261
)}
262
<A href="https://github.com/sagemathinc/cocalc/discussions">
263
join a discussion on GitHub
264
</A>
265
, and add more collaborators to this project below.
266
<AddCollaborators project_id={project_id} autoFocus where="side-chat" />
267
<div style={{ color: COLORS.GRAY_M }}>
268
(Collaborators have access to all files in this project.)
269
</div>
270
</div>
271
);
272
}
273
274
function CollabList({ project, addCollab, actions }) {
275
return (
276
<div
277
style={
278
!addCollab
279
? {
280
maxHeight: "1.7em",
281
whiteSpace: "nowrap",
282
overflow: "hidden",
283
textOverflow: "ellipsis",
284
cursor: "pointer",
285
}
286
: { cursor: "pointer" }
287
}
288
onClick={() => actions.setState({ add_collab: !addCollab })}
289
>
290
<div style={{ width: "16px", display: "inline-block" }}>
291
<Icon name={addCollab ? "caret-down" : "caret-right"} />
292
</div>
293
<span style={{ color: COLORS.GRAY_M, fontSize: "10pt" }}>
294
<ProjectUsers
295
project={project}
296
none={<span>Add people to work with...</span>}
297
/>
298
</span>
299
</div>
300
);
301
}
302
303