Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/support/create.tsx
1449 views
1
import {
2
Alert,
3
Button,
4
Divider,
5
Input,
6
Layout,
7
Modal,
8
Radio,
9
Space,
10
} from "antd";
11
import { useRouter } from "next/router";
12
import { ReactNode, useRef, useState } from "react";
13
14
import { Icon } from "@cocalc/frontend/components/icon";
15
import { is_valid_email_address as isValidEmailAddress } from "@cocalc/util/misc";
16
import { COLORS } from "@cocalc/util/theme";
17
import { Paragraph, Title } from "components/misc";
18
import A from "components/misc/A";
19
import ChatGPTHelp from "components/openai/chatgpt-help";
20
import CodeMirror from "components/share/codemirror";
21
import Loading from "components/share/loading";
22
import SiteName from "components/share/site-name";
23
import { VideoItem } from "components/videos";
24
import apiPost from "lib/api/post";
25
import { MAX_WIDTH } from "lib/config";
26
import { useCustomize } from "lib/customize";
27
import getBrowserInfo from "./browser-info";
28
import RecentFiles from "./recent-files";
29
import { Type } from "./tickets";
30
import { NoZendesk } from "./util";
31
32
const CHATGPT_DISABLED = true;
33
const MIN_BODY_LENGTH = 16;
34
35
function VSpace({ children }) {
36
return (
37
<Space direction="vertical" style={{ width: "100%", fontSize: "12pt" }}>
38
{children}
39
</Space>
40
);
41
}
42
43
export type Type = "problem" | "question" | "task" | "purchase" | "chat";
44
45
function stringToType(s?: any): Type {
46
if (
47
s === "problem" ||
48
s === "question" ||
49
s === "task" ||
50
s === "purchase" ||
51
s === "chat"
52
)
53
return s;
54
return "problem"; // default;
55
}
56
57
export default function Create() {
58
const {
59
account,
60
onCoCalcCom,
61
helpEmail,
62
openaiEnabled,
63
siteName,
64
zendesk,
65
supportVideoCall,
66
} = useCustomize();
67
const router = useRouter();
68
// The URL the user was viewing when they requested support.
69
// This could easily be blank, but if it is set it can be useful.
70
const { url } = router.query;
71
const [files, setFiles] = useState<{ project_id: string; path?: string }[]>(
72
[],
73
);
74
const [type, setType] = useState<Type>(stringToType(router.query.type));
75
const [email, setEmail] = useState<string>(account?.email_address ?? "");
76
const [body, setBody] = useState<string>(
77
router.query.body ? `${router.query.body}` : "",
78
);
79
const required = router.query.required ? `${router.query.required}` : "";
80
const [subject, setSubject] = useState<string>(
81
router.query.subject ? `${router.query.subject}` : "",
82
);
83
84
const [submitError, setSubmitError] = useState<ReactNode>("");
85
const [submitting, setSubmitting] = useState<boolean>(false);
86
const [success, setSuccess] = useState<ReactNode>("");
87
88
const showExtra = router.query.hideExtra != "true";
89
90
// hasRequired means "has the required information", which
91
// means that body does NOT have required in it!
92
const hasRequired = !required || !body.includes(required);
93
94
const submittable = useRef<boolean>(false);
95
submittable.current = !!(
96
!submitting &&
97
!submitError &&
98
!success &&
99
isValidEmailAddress(email) &&
100
subject &&
101
(body ?? "").length >= MIN_BODY_LENGTH &&
102
hasRequired
103
);
104
105
if (!zendesk) {
106
return <NoZendesk />;
107
}
108
109
async function createSupportTicket() {
110
const info = getBrowserInfo();
111
if (router.query.context) {
112
// used to pass context info along in the url when
113
// creating a support ticket,
114
// e.g., from the crash reporter.
115
info.context = `${router.query.context}`;
116
}
117
const options = { type, files, email, body, url, subject, info };
118
setSubmitError("");
119
let result;
120
try {
121
setSubmitting(true);
122
result = await apiPost("/support/create-ticket", { options });
123
} catch (err) {
124
setSubmitError(err.message);
125
return;
126
} finally {
127
setSubmitting(false);
128
}
129
setSuccess(
130
<div>
131
<p>
132
Please save this URL: <A href={result.url}>{result.url}</A>
133
</p>
134
<p>
135
You can also see the{" "}
136
<A href="/support/tickets">status of your support tickets</A>.
137
</p>
138
</div>,
139
);
140
}
141
142
function renderChat() {
143
if (type === "chat" && supportVideoCall) {
144
return (
145
<Alert
146
type="info"
147
showIcon={false}
148
description={
149
<Paragraph style={{ fontSize: "16px" }}>
150
Please describe what you want to discuss in the{" "}
151
<A href={supportVideoCall}>video chat</A>. We will then contact
152
you to confirm the time.
153
</Paragraph>
154
}
155
message={
156
<Title level={2}>
157
<Icon name="video-camera" /> You can{" "}
158
<A href={supportVideoCall}>book a video chat</A> with us.
159
</Title>
160
}
161
/>
162
);
163
} else {
164
return (
165
<>
166
<b>
167
<Status
168
done={body && body.length >= MIN_BODY_LENGTH && hasRequired}
169
/>{" "}
170
Description
171
</b>
172
<div
173
style={{
174
marginLeft: "30px",
175
borderLeft: "1px solid lightgrey",
176
paddingLeft: "15px",
177
}}
178
>
179
{type === "problem" && <Problem onChange={setBody} />}
180
{type === "question" && (
181
<Question onChange={setBody} defaultValue={body} />
182
)}
183
{type === "purchase" && (
184
<Purchase
185
onChange={setBody}
186
defaultValue={body}
187
showExtra={showExtra}
188
/>
189
)}
190
{type === "task" && <Task onChange={setBody} />}
191
</div>
192
</>
193
);
194
}
195
}
196
197
return (
198
<Layout.Content style={{ backgroundColor: "white" }}>
199
<div
200
style={{
201
maxWidth: MAX_WIDTH,
202
margin: "15px auto",
203
padding: "15px",
204
backgroundColor: "white",
205
color: COLORS.GRAY_D,
206
}}
207
>
208
<Title level={1} style={{ textAlign: "center" }}>
209
{router.query.title ?? "Create a New Support Ticket"}
210
</Title>
211
{showExtra && (
212
<>
213
<Space>
214
<Space direction="vertical" size="large">
215
<Paragraph style={{ fontSize: "16px" }}>
216
Create a new support ticket below or{" "}
217
<A href="/support/tickets">
218
check the status of your support tickets
219
</A>
220
.
221
</Paragraph>
222
{helpEmail ? (
223
<Paragraph style={{ fontSize: "16px" }}>
224
You can also email us directly at{" "}
225
<A href={`mailto:${helpEmail}`}>{helpEmail}</A>.
226
</Paragraph>
227
) : undefined}
228
{supportVideoCall ? (
229
<Paragraph style={{ fontSize: "16px" }}>
230
Alternatively, feel free to{" "}
231
<A href={supportVideoCall}>book a video call</A> with us.
232
</Paragraph>
233
) : undefined}
234
</Space>
235
<VideoItem
236
width={600}
237
style={{ margin: "15px 0", width: "600px" }}
238
id={"4Ef9sxX59XM"}
239
/>
240
</Space>
241
{openaiEnabled && onCoCalcCom && !CHATGPT_DISABLED ? (
242
<ChatGPT siteName={siteName} />
243
) : undefined}
244
<FAQ />
245
<Title level={2}>Create Your Ticket</Title>
246
<Instructions />
247
<Divider>Support Ticket</Divider>
248
</>
249
)}
250
<form>
251
<VSpace>
252
<b>
253
<Status done={isValidEmailAddress(email)} /> Your Email Address
254
</b>
255
<Input
256
prefix={
257
<Icon name="envelope" style={{ color: "rgba(0,0,0,.25)" }} />
258
}
259
defaultValue={email}
260
placeholder="Email address..."
261
style={{ maxWidth: "500px" }}
262
onChange={(e) => setEmail(e.target.value)}
263
/>
264
<br />
265
<b>
266
<Status done={subject} /> Subject
267
</b>
268
<Input
269
placeholder="Summarize what this is about..."
270
onChange={(e) => setSubject(e.target.value)}
271
defaultValue={subject}
272
/>
273
<br />
274
<b>
275
Is this a <i>Problem</i>, <i>Question</i>, or{" "}
276
<i>Software Install Task</i>?
277
</b>
278
<Radio.Group
279
name="radiogroup"
280
defaultValue={type}
281
onChange={(e) => setType(e.target.value)}
282
>
283
<VSpace>
284
<Radio value={"problem"}>
285
<Type type="problem" /> Something is not working the way I
286
think it should work.
287
</Radio>
288
<Radio value={"question"}>
289
<Type type="question" /> I have a question about billing,
290
functionality, teaching, something not working, etc.
291
</Radio>
292
<Radio value={"task"}>
293
<Type type="task" /> Is it possible for you to install some
294
software that I need in order to use <SiteName />?
295
</Radio>
296
<Radio value={"purchase"}>
297
<Type type="purchase" /> I have a question regarding
298
purchasing a product.
299
</Radio>
300
<Radio value={"chat"}>
301
<Type type="chat" /> I would like to schedule a video chat.
302
</Radio>
303
</VSpace>
304
</Radio.Group>
305
<br />
306
{showExtra && type !== "purchase" && type != "chat" && (
307
<>
308
<Files onChange={setFiles} />
309
<br />
310
</>
311
)}
312
{renderChat()}
313
</VSpace>
314
315
<div style={{ textAlign: "center", marginTop: "30px" }}>
316
{!hasRequired && (
317
<Alert
318
showIcon
319
style={{ margin: "15px 30px" }}
320
type="error"
321
description={`You must replace the text '${required}' everywhere above with the requested information.`}
322
/>
323
)}
324
{type != "chat" && (
325
<Button
326
shape="round"
327
size="large"
328
disabled={!submittable.current}
329
type="primary"
330
onClick={createSupportTicket}
331
>
332
<Icon name="paper-plane" />{" "}
333
{submitting
334
? "Submitting..."
335
: success
336
? "Thank you for creating a ticket"
337
: submitError
338
? "Close the error box to try again"
339
: !isValidEmailAddress(email)
340
? "Enter Valid Email Address above"
341
: !subject
342
? "Enter Subject above"
343
: (body ?? "").length < MIN_BODY_LENGTH
344
? `Describe your ${type} in detail above`
345
: "Create Support Ticket"}
346
</Button>
347
)}
348
{submitting && <Loading style={{ fontSize: "32pt" }} />}
349
{submitError && (
350
<div>
351
<Alert
352
type="error"
353
message="Error creating support ticket"
354
description={submitError}
355
closable
356
showIcon
357
onClose={() => setSubmitError("")}
358
style={{ margin: "15px auto", maxWidth: "500px" }}
359
/>
360
<br />
361
{helpEmail ? (
362
<>
363
If you continue to have problems, email us directly at{" "}
364
<A href={`mailto:${helpEmail}`}>{helpEmail}</A>.
365
</>
366
) : undefined}
367
</div>
368
)}
369
{success && (
370
<Alert
371
type="success"
372
message="Successfully created support ticket"
373
description={success}
374
onClose={() => {
375
// simplest way to reset all the information in the form.
376
router.reload();
377
}}
378
closable
379
showIcon
380
style={{ margin: "15px auto", maxWidth: "500px" }}
381
/>
382
)}
383
</div>
384
</form>
385
{type !== "chat" && (
386
<Paragraph style={{ marginTop: "30px" }}>
387
After submitting this, you'll receive a link, which you should save
388
until you receive a confirmation email. You can also{" "}
389
<A href="/support/tickets">check the status of your tickets here</A>
390
.
391
</Paragraph>
392
)}
393
</div>
394
</Layout.Content>
395
);
396
}
397
398
function Files({ onChange }) {
399
return (
400
<VSpace>
401
<b>Relevant Files</b>
402
Select any relevant projects and files below. This will make it much
403
easier for us to quickly understand your problem.
404
<RecentFiles interval="1 day" onChange={onChange} />
405
</VSpace>
406
);
407
}
408
409
function Problem({ onChange }) {
410
const answers = useRef<[string, string, string]>(["", "", ""]);
411
function update(i: 0 | 1 | 2, value: string): void {
412
answers.current[i] = value;
413
onChange?.(answers.current.join("\n\n\n").trim());
414
}
415
416
return (
417
<VSpace>
418
<b>What did you do exactly?</b>
419
<Input.TextArea
420
rows={3}
421
placeholder="Describe what you did..."
422
onChange={(e) =>
423
update(
424
0,
425
e.target.value
426
? "\n\nWHAT DID YOU DO EXACTLY?\n\n" + e.target.value
427
: "",
428
)
429
}
430
/>
431
<br />
432
<b>What happened?</b>
433
<Input.TextArea
434
rows={3}
435
placeholder="Tell us what happened..."
436
onChange={(e) =>
437
update(
438
1,
439
e.target.value ? "\n\nWHAT HAPPENED?\n\n" + e.target.value : "",
440
)
441
}
442
/>
443
<br />
444
<b>How did this differ from what you expected?</b>
445
<Input.TextArea
446
rows={3}
447
placeholder="Explain how this differs from what you expected..."
448
onChange={(e) =>
449
update(
450
2,
451
e.target.value
452
? "\n\nHOW DID THIS DIFFER FROM WHAT YOU EXPECTED?\n\n" +
453
e.target.value
454
: "",
455
)
456
}
457
/>
458
</VSpace>
459
);
460
}
461
462
function Question({ defaultValue, onChange }) {
463
return (
464
<Input.TextArea
465
rows={8}
466
defaultValue={defaultValue}
467
placeholder="Your question..."
468
onChange={(e) => onChange(e.target.value)}
469
/>
470
);
471
}
472
473
function Purchase({ defaultValue, onChange, showExtra }) {
474
return (
475
<>
476
{showExtra && (
477
<Paragraph>
478
Please describe what you want to purchase. We need some context in
479
order to guide you. In particular:
480
<ul>
481
<li>
482
The expected number of projects: this is either the number of
483
users, or how many projects they'll collectively be using.
484
</li>
485
<li>
486
The kind of workload: this ranges from student projects with
487
minimal resource requirements to large and resource intensive
488
research projects.
489
</li>
490
<li>How long you expect to use the services.</li>
491
<li>
492
Your type of organization: i.e. if an academic discount applies to
493
you.
494
</li>
495
</ul>
496
</Paragraph>
497
)}
498
<Input.TextArea
499
rows={8}
500
defaultValue={defaultValue}
501
placeholder="Your purchase request..."
502
onChange={(e) => onChange(e.target.value)}
503
/>
504
</>
505
);
506
}
507
508
function Task({ onChange }) {
509
const answers = useRef<[string, string, string]>(["", "", ""]);
510
function update(i: 0 | 1 | 2, value: string): void {
511
answers.current[i] = value;
512
onChange?.(answers.current.join("\n\n\n").trim());
513
}
514
515
const [showWestPoint, setShowWestPoint] = useState<boolean>(false);
516
517
return (
518
<div>
519
<Modal
520
width="700px"
521
open={showWestPoint}
522
onCancel={() => setShowWestPoint(false)}
523
onOk={() => setShowWestPoint(false)}
524
title={
525
<div>
526
A question about CoCalc ...
527
<A href="https://www.westpoint.edu/mathematical-sciences/profile/joseph_lindquist">
528
<div
529
style={{
530
fontSize: "10px",
531
float: "right",
532
width: "125px",
533
margin: "0 20px",
534
}}
535
>
536
<img
537
style={{ width: "125px" }}
538
src="https://s3.amazonaws.com/usma-media/styles/profile_image_display/s3/inline-images/academics/academic_departments/mathematical_sciences/images/profiles/COL%20JOE%20LINDQUIST.jpg?itok=r9vjncwh"
539
/>
540
Colonel Joe Lindquist
541
<br />
542
West Point
543
</div>
544
</A>
545
</div>
546
}
547
>
548
<b>WHAT SOFTWARE DO YOU NEED?</b>
549
<br />
550
Hi Team! I'm getting ready to kick off our short course at West Point
551
that will deal with Natural Language Processing. We're still sorting out
552
the purchase request, but expect it to be complete in the next day or
553
so. It looks like you have the "big" packages installed that we will be
554
exploring... Huggingface, Transformers, NLTK, WordBlob... but another
555
package that I was hoping to use is vadersentiment (
556
<A href="https://pypi.org/project/vaderSentiment/">
557
https://pypi.org/project/vaderSentiment/
558
</A>
559
).
560
<br />
561
<br />
562
<b>HOW DO YOU PLAN TO USE THIS SOFTWARE?</b>
563
<br />
564
The course begins on 15MAR and I'd love to be able to use it for this.
565
I'm happy to assume some guidance on how to best incorporate this into
566
CoCalc if unable to install the package.
567
<br />
568
<br />
569
<b>HOW CAN WE TEST THAT THE SOFTWARE IS PROPERLY INSTALLED?</b>
570
<CodeMirror
571
fontSize={12}
572
lineNumbers={false}
573
filename="a.py"
574
content={`from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
575
sid_obj = SentimentIntensityAnalyzer()
576
text = "CoCalc is an amazing platform for students to learn how to understand NLP!"
577
print(sid_obj.polarity_scores(text))`}
578
/>
579
<br />
580
This should return:
581
<CodeMirror
582
fontSize={12}
583
lineNumbers={false}
584
filename="a.json"
585
content={
586
"{'neg': 0.0, 'neu': 0.746, 'pos': 0.254, 'compound': 0.6239}"
587
}
588
/>
589
<br />
590
One Day Later
591
<br />
592
You guys are fantastic! Such a quick turn-around. Please feel free to
593
use the request in any fashion you wish 😊
594
<br />
595
By the way… in case you were wondering, “You guys are fantastic!” has a
596
compound polarity score of 0.598 😊. I used it in CoCalc to test the
597
update.
598
</Modal>
599
Each <SiteName /> project is a Docker image running Ubuntu Linux on 64-bit
600
x86 hardware, so it is possible for us to install most standard Linux
601
software, and we have already installed{" "}
602
<A href="/software">a huge amount</A>. If there is something you need that
603
is missing, let us know below. You can also{" "}
604
<a onClick={() => setShowWestPoint(true)}>
605
view a recent ticket from West Point
606
</a>{" "}
607
for an example install request.
608
<br />
609
<br />
610
<b>What software do you need?</b> In particular, if this is a Python
611
library, explain which of the{" "}
612
<A href="software/python">many Python environments</A> you need it
613
installed into and why you can't just{" "}
614
<A href="https://doc.cocalc.com/howto/install-python-lib.html">
615
install it yourself
616
</A>
617
.
618
<br />
619
<Input.TextArea
620
style={{ marginTop: "10px" }}
621
rows={4}
622
placeholder="Describe what software you need installed..."
623
onChange={(e) =>
624
update(
625
0,
626
e.target.value
627
? "\n\nWHAT SOFTWARE DO YOU NEED?\n\n" + e.target.value
628
: "",
629
)
630
}
631
/>
632
<br />
633
<br />
634
<br />
635
<b>How do you plan to use this software?</b> For example, does it need to
636
be installed across <SiteName /> for a course you are teaching that starts
637
in 3 weeks?
638
<br />
639
<Input.TextArea
640
style={{ marginTop: "10px" }}
641
rows={3}
642
placeholder="Explain how you will use the software ..."
643
onChange={(e) =>
644
update(
645
1,
646
e.target.value
647
? "\n\nHOW DO YOU PLAN TO USE THIS SOFTWARE?\n\n" + e.target.value
648
: "",
649
)
650
}
651
/>
652
<br />
653
<br />
654
<br />
655
<b>How can we test that the software is properly installed?</b>
656
<br />
657
<Input.TextArea
658
style={{ marginTop: "10px" }}
659
rows={3}
660
placeholder="Explain how we can test the software..."
661
onChange={(e) =>
662
update(
663
2,
664
e.target.value
665
? "\n\nHOW CAN WE TEST THAT THE SOFTWARE IS PROPERLY INSTALLED?\n\n" +
666
e.target.value
667
: "",
668
)
669
}
670
/>
671
</div>
672
);
673
}
674
function Instructions() {
675
return (
676
<div>
677
<p>
678
If the above links don't help you solve your problem, please create a
679
support ticket below. Support is currently available in{" "}
680
<b>English, German, and Russian</b> only.
681
</p>
682
</div>
683
);
684
}
685
686
function ChatGPT({ siteName }) {
687
return (
688
<div style={{ margin: "15px 0 20px 0" }}>
689
<Title level={2}>ChatGPT</Title>
690
<div style={{ color: "#666" }}>
691
If you have a question about how to do something using {siteName},
692
ChatGPT might save you some time:
693
</div>
694
<ChatGPTHelp style={{ marginTop: "15px" }} tag={"support"} />
695
</div>
696
);
697
}
698
699
function FAQ() {
700
return (
701
<div>
702
<Title level={2}>Helpful Links</Title>
703
<Alert
704
message={""}
705
style={{ margin: "20px 0" }}
706
type="warning"
707
description={
708
<ul style={{ marginBottom: 0, fontSize: "11pt" }}>
709
<li>
710
<A href="https://doc.cocalc.com/">The CoCalc Manual</A>
711
</li>
712
<li>
713
<A href="https://github.com/sagemathinc/cocalc/issues">
714
Bug reports
715
</A>
716
</li>
717
<li>
718
<A href="https://github.com/sagemathinc/cocalc/discussions">
719
The CoCalc Discussion Forum
720
</A>
721
</li>
722
<li>
723
{" "}
724
<A href="https://doc.cocalc.com/howto/missing-project.html">
725
Help: My file or project appears to be missing!
726
</A>{" "}
727
</li>
728
<li>
729
{" "}
730
I have{" "}
731
<A href="https://doc.cocalc.com/howto/sage-question.html">
732
general questions about SageMath...
733
</A>
734
</li>
735
</ul>
736
}
737
/>
738
</div>
739
);
740
}
741
742
function Status({ done }) {
743
return (
744
<Icon
745
style={{
746
color: done ? "green" : "red",
747
fontWeight: "bold",
748
fontSize: "12pt",
749
}}
750
name={done ? "check" : "arrow-right"}
751
/>
752
);
753
}
754
755