Path: blob/master/src/packages/next/components/support/create.tsx
1449 views
import {1Alert,2Button,3Divider,4Input,5Layout,6Modal,7Radio,8Space,9} from "antd";10import { useRouter } from "next/router";11import { ReactNode, useRef, useState } from "react";1213import { Icon } from "@cocalc/frontend/components/icon";14import { is_valid_email_address as isValidEmailAddress } from "@cocalc/util/misc";15import { COLORS } from "@cocalc/util/theme";16import { Paragraph, Title } from "components/misc";17import A from "components/misc/A";18import ChatGPTHelp from "components/openai/chatgpt-help";19import CodeMirror from "components/share/codemirror";20import Loading from "components/share/loading";21import SiteName from "components/share/site-name";22import { VideoItem } from "components/videos";23import apiPost from "lib/api/post";24import { MAX_WIDTH } from "lib/config";25import { useCustomize } from "lib/customize";26import getBrowserInfo from "./browser-info";27import RecentFiles from "./recent-files";28import { Type } from "./tickets";29import { NoZendesk } from "./util";3031const CHATGPT_DISABLED = true;32const MIN_BODY_LENGTH = 16;3334function VSpace({ children }) {35return (36<Space direction="vertical" style={{ width: "100%", fontSize: "12pt" }}>37{children}38</Space>39);40}4142export type Type = "problem" | "question" | "task" | "purchase" | "chat";4344function stringToType(s?: any): Type {45if (46s === "problem" ||47s === "question" ||48s === "task" ||49s === "purchase" ||50s === "chat"51)52return s;53return "problem"; // default;54}5556export default function Create() {57const {58account,59onCoCalcCom,60helpEmail,61openaiEnabled,62siteName,63zendesk,64supportVideoCall,65} = useCustomize();66const router = useRouter();67// The URL the user was viewing when they requested support.68// This could easily be blank, but if it is set it can be useful.69const { url } = router.query;70const [files, setFiles] = useState<{ project_id: string; path?: string }[]>(71[],72);73const [type, setType] = useState<Type>(stringToType(router.query.type));74const [email, setEmail] = useState<string>(account?.email_address ?? "");75const [body, setBody] = useState<string>(76router.query.body ? `${router.query.body}` : "",77);78const required = router.query.required ? `${router.query.required}` : "";79const [subject, setSubject] = useState<string>(80router.query.subject ? `${router.query.subject}` : "",81);8283const [submitError, setSubmitError] = useState<ReactNode>("");84const [submitting, setSubmitting] = useState<boolean>(false);85const [success, setSuccess] = useState<ReactNode>("");8687const showExtra = router.query.hideExtra != "true";8889// hasRequired means "has the required information", which90// means that body does NOT have required in it!91const hasRequired = !required || !body.includes(required);9293const submittable = useRef<boolean>(false);94submittable.current = !!(95!submitting &&96!submitError &&97!success &&98isValidEmailAddress(email) &&99subject &&100(body ?? "").length >= MIN_BODY_LENGTH &&101hasRequired102);103104if (!zendesk) {105return <NoZendesk />;106}107108async function createSupportTicket() {109const info = getBrowserInfo();110if (router.query.context) {111// used to pass context info along in the url when112// creating a support ticket,113// e.g., from the crash reporter.114info.context = `${router.query.context}`;115}116const options = { type, files, email, body, url, subject, info };117setSubmitError("");118let result;119try {120setSubmitting(true);121result = await apiPost("/support/create-ticket", { options });122} catch (err) {123setSubmitError(err.message);124return;125} finally {126setSubmitting(false);127}128setSuccess(129<div>130<p>131Please save this URL: <A href={result.url}>{result.url}</A>132</p>133<p>134You can also see the{" "}135<A href="/support/tickets">status of your support tickets</A>.136</p>137</div>,138);139}140141function renderChat() {142if (type === "chat" && supportVideoCall) {143return (144<Alert145type="info"146showIcon={false}147description={148<Paragraph style={{ fontSize: "16px" }}>149Please describe what you want to discuss in the{" "}150<A href={supportVideoCall}>video chat</A>. We will then contact151you to confirm the time.152</Paragraph>153}154message={155<Title level={2}>156<Icon name="video-camera" /> You can{" "}157<A href={supportVideoCall}>book a video chat</A> with us.158</Title>159}160/>161);162} else {163return (164<>165<b>166<Status167done={body && body.length >= MIN_BODY_LENGTH && hasRequired}168/>{" "}169Description170</b>171<div172style={{173marginLeft: "30px",174borderLeft: "1px solid lightgrey",175paddingLeft: "15px",176}}177>178{type === "problem" && <Problem onChange={setBody} />}179{type === "question" && (180<Question onChange={setBody} defaultValue={body} />181)}182{type === "purchase" && (183<Purchase184onChange={setBody}185defaultValue={body}186showExtra={showExtra}187/>188)}189{type === "task" && <Task onChange={setBody} />}190</div>191</>192);193}194}195196return (197<Layout.Content style={{ backgroundColor: "white" }}>198<div199style={{200maxWidth: MAX_WIDTH,201margin: "15px auto",202padding: "15px",203backgroundColor: "white",204color: COLORS.GRAY_D,205}}206>207<Title level={1} style={{ textAlign: "center" }}>208{router.query.title ?? "Create a New Support Ticket"}209</Title>210{showExtra && (211<>212<Space>213<Space direction="vertical" size="large">214<Paragraph style={{ fontSize: "16px" }}>215Create a new support ticket below or{" "}216<A href="/support/tickets">217check the status of your support tickets218</A>219.220</Paragraph>221{helpEmail ? (222<Paragraph style={{ fontSize: "16px" }}>223You can also email us directly at{" "}224<A href={`mailto:${helpEmail}`}>{helpEmail}</A>.225</Paragraph>226) : undefined}227{supportVideoCall ? (228<Paragraph style={{ fontSize: "16px" }}>229Alternatively, feel free to{" "}230<A href={supportVideoCall}>book a video call</A> with us.231</Paragraph>232) : undefined}233</Space>234<VideoItem235width={600}236style={{ margin: "15px 0", width: "600px" }}237id={"4Ef9sxX59XM"}238/>239</Space>240{openaiEnabled && onCoCalcCom && !CHATGPT_DISABLED ? (241<ChatGPT siteName={siteName} />242) : undefined}243<FAQ />244<Title level={2}>Create Your Ticket</Title>245<Instructions />246<Divider>Support Ticket</Divider>247</>248)}249<form>250<VSpace>251<b>252<Status done={isValidEmailAddress(email)} /> Your Email Address253</b>254<Input255prefix={256<Icon name="envelope" style={{ color: "rgba(0,0,0,.25)" }} />257}258defaultValue={email}259placeholder="Email address..."260style={{ maxWidth: "500px" }}261onChange={(e) => setEmail(e.target.value)}262/>263<br />264<b>265<Status done={subject} /> Subject266</b>267<Input268placeholder="Summarize what this is about..."269onChange={(e) => setSubject(e.target.value)}270defaultValue={subject}271/>272<br />273<b>274Is this a <i>Problem</i>, <i>Question</i>, or{" "}275<i>Software Install Task</i>?276</b>277<Radio.Group278name="radiogroup"279defaultValue={type}280onChange={(e) => setType(e.target.value)}281>282<VSpace>283<Radio value={"problem"}>284<Type type="problem" /> Something is not working the way I285think it should work.286</Radio>287<Radio value={"question"}>288<Type type="question" /> I have a question about billing,289functionality, teaching, something not working, etc.290</Radio>291<Radio value={"task"}>292<Type type="task" /> Is it possible for you to install some293software that I need in order to use <SiteName />?294</Radio>295<Radio value={"purchase"}>296<Type type="purchase" /> I have a question regarding297purchasing a product.298</Radio>299<Radio value={"chat"}>300<Type type="chat" /> I would like to schedule a video chat.301</Radio>302</VSpace>303</Radio.Group>304<br />305{showExtra && type !== "purchase" && type != "chat" && (306<>307<Files onChange={setFiles} />308<br />309</>310)}311{renderChat()}312</VSpace>313314<div style={{ textAlign: "center", marginTop: "30px" }}>315{!hasRequired && (316<Alert317showIcon318style={{ margin: "15px 30px" }}319type="error"320description={`You must replace the text '${required}' everywhere above with the requested information.`}321/>322)}323{type != "chat" && (324<Button325shape="round"326size="large"327disabled={!submittable.current}328type="primary"329onClick={createSupportTicket}330>331<Icon name="paper-plane" />{" "}332{submitting333? "Submitting..."334: success335? "Thank you for creating a ticket"336: submitError337? "Close the error box to try again"338: !isValidEmailAddress(email)339? "Enter Valid Email Address above"340: !subject341? "Enter Subject above"342: (body ?? "").length < MIN_BODY_LENGTH343? `Describe your ${type} in detail above`344: "Create Support Ticket"}345</Button>346)}347{submitting && <Loading style={{ fontSize: "32pt" }} />}348{submitError && (349<div>350<Alert351type="error"352message="Error creating support ticket"353description={submitError}354closable355showIcon356onClose={() => setSubmitError("")}357style={{ margin: "15px auto", maxWidth: "500px" }}358/>359<br />360{helpEmail ? (361<>362If you continue to have problems, email us directly at{" "}363<A href={`mailto:${helpEmail}`}>{helpEmail}</A>.364</>365) : undefined}366</div>367)}368{success && (369<Alert370type="success"371message="Successfully created support ticket"372description={success}373onClose={() => {374// simplest way to reset all the information in the form.375router.reload();376}}377closable378showIcon379style={{ margin: "15px auto", maxWidth: "500px" }}380/>381)}382</div>383</form>384{type !== "chat" && (385<Paragraph style={{ marginTop: "30px" }}>386After submitting this, you'll receive a link, which you should save387until you receive a confirmation email. You can also{" "}388<A href="/support/tickets">check the status of your tickets here</A>389.390</Paragraph>391)}392</div>393</Layout.Content>394);395}396397function Files({ onChange }) {398return (399<VSpace>400<b>Relevant Files</b>401Select any relevant projects and files below. This will make it much402easier for us to quickly understand your problem.403<RecentFiles interval="1 day" onChange={onChange} />404</VSpace>405);406}407408function Problem({ onChange }) {409const answers = useRef<[string, string, string]>(["", "", ""]);410function update(i: 0 | 1 | 2, value: string): void {411answers.current[i] = value;412onChange?.(answers.current.join("\n\n\n").trim());413}414415return (416<VSpace>417<b>What did you do exactly?</b>418<Input.TextArea419rows={3}420placeholder="Describe what you did..."421onChange={(e) =>422update(4230,424e.target.value425? "\n\nWHAT DID YOU DO EXACTLY?\n\n" + e.target.value426: "",427)428}429/>430<br />431<b>What happened?</b>432<Input.TextArea433rows={3}434placeholder="Tell us what happened..."435onChange={(e) =>436update(4371,438e.target.value ? "\n\nWHAT HAPPENED?\n\n" + e.target.value : "",439)440}441/>442<br />443<b>How did this differ from what you expected?</b>444<Input.TextArea445rows={3}446placeholder="Explain how this differs from what you expected..."447onChange={(e) =>448update(4492,450e.target.value451? "\n\nHOW DID THIS DIFFER FROM WHAT YOU EXPECTED?\n\n" +452e.target.value453: "",454)455}456/>457</VSpace>458);459}460461function Question({ defaultValue, onChange }) {462return (463<Input.TextArea464rows={8}465defaultValue={defaultValue}466placeholder="Your question..."467onChange={(e) => onChange(e.target.value)}468/>469);470}471472function Purchase({ defaultValue, onChange, showExtra }) {473return (474<>475{showExtra && (476<Paragraph>477Please describe what you want to purchase. We need some context in478order to guide you. In particular:479<ul>480<li>481The expected number of projects: this is either the number of482users, or how many projects they'll collectively be using.483</li>484<li>485The kind of workload: this ranges from student projects with486minimal resource requirements to large and resource intensive487research projects.488</li>489<li>How long you expect to use the services.</li>490<li>491Your type of organization: i.e. if an academic discount applies to492you.493</li>494</ul>495</Paragraph>496)}497<Input.TextArea498rows={8}499defaultValue={defaultValue}500placeholder="Your purchase request..."501onChange={(e) => onChange(e.target.value)}502/>503</>504);505}506507function Task({ onChange }) {508const answers = useRef<[string, string, string]>(["", "", ""]);509function update(i: 0 | 1 | 2, value: string): void {510answers.current[i] = value;511onChange?.(answers.current.join("\n\n\n").trim());512}513514const [showWestPoint, setShowWestPoint] = useState<boolean>(false);515516return (517<div>518<Modal519width="700px"520open={showWestPoint}521onCancel={() => setShowWestPoint(false)}522onOk={() => setShowWestPoint(false)}523title={524<div>525A question about CoCalc ...526<A href="https://www.westpoint.edu/mathematical-sciences/profile/joseph_lindquist">527<div528style={{529fontSize: "10px",530float: "right",531width: "125px",532margin: "0 20px",533}}534>535<img536style={{ width: "125px" }}537src="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"538/>539Colonel Joe Lindquist540<br />541West Point542</div>543</A>544</div>545}546>547<b>WHAT SOFTWARE DO YOU NEED?</b>548<br />549Hi Team! I'm getting ready to kick off our short course at West Point550that will deal with Natural Language Processing. We're still sorting out551the purchase request, but expect it to be complete in the next day or552so. It looks like you have the "big" packages installed that we will be553exploring... Huggingface, Transformers, NLTK, WordBlob... but another554package that I was hoping to use is vadersentiment (555<A href="https://pypi.org/project/vaderSentiment/">556https://pypi.org/project/vaderSentiment/557</A>558).559<br />560<br />561<b>HOW DO YOU PLAN TO USE THIS SOFTWARE?</b>562<br />563The course begins on 15MAR and I'd love to be able to use it for this.564I'm happy to assume some guidance on how to best incorporate this into565CoCalc if unable to install the package.566<br />567<br />568<b>HOW CAN WE TEST THAT THE SOFTWARE IS PROPERLY INSTALLED?</b>569<CodeMirror570fontSize={12}571lineNumbers={false}572filename="a.py"573content={`from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer574sid_obj = SentimentIntensityAnalyzer()575text = "CoCalc is an amazing platform for students to learn how to understand NLP!"576print(sid_obj.polarity_scores(text))`}577/>578<br />579This should return:580<CodeMirror581fontSize={12}582lineNumbers={false}583filename="a.json"584content={585"{'neg': 0.0, 'neu': 0.746, 'pos': 0.254, 'compound': 0.6239}"586}587/>588<br />589One Day Later590<br />591You guys are fantastic! Such a quick turn-around. Please feel free to592use the request in any fashion you wish 😊593<br />594By the way… in case you were wondering, “You guys are fantastic!” has a595compound polarity score of 0.598 😊. I used it in CoCalc to test the596update.597</Modal>598Each <SiteName /> project is a Docker image running Ubuntu Linux on 64-bit599x86 hardware, so it is possible for us to install most standard Linux600software, and we have already installed{" "}601<A href="/software">a huge amount</A>. If there is something you need that602is missing, let us know below. You can also{" "}603<a onClick={() => setShowWestPoint(true)}>604view a recent ticket from West Point605</a>{" "}606for an example install request.607<br />608<br />609<b>What software do you need?</b> In particular, if this is a Python610library, explain which of the{" "}611<A href="software/python">many Python environments</A> you need it612installed into and why you can't just{" "}613<A href="https://doc.cocalc.com/howto/install-python-lib.html">614install it yourself615</A>616.617<br />618<Input.TextArea619style={{ marginTop: "10px" }}620rows={4}621placeholder="Describe what software you need installed..."622onChange={(e) =>623update(6240,625e.target.value626? "\n\nWHAT SOFTWARE DO YOU NEED?\n\n" + e.target.value627: "",628)629}630/>631<br />632<br />633<br />634<b>How do you plan to use this software?</b> For example, does it need to635be installed across <SiteName /> for a course you are teaching that starts636in 3 weeks?637<br />638<Input.TextArea639style={{ marginTop: "10px" }}640rows={3}641placeholder="Explain how you will use the software ..."642onChange={(e) =>643update(6441,645e.target.value646? "\n\nHOW DO YOU PLAN TO USE THIS SOFTWARE?\n\n" + e.target.value647: "",648)649}650/>651<br />652<br />653<br />654<b>How can we test that the software is properly installed?</b>655<br />656<Input.TextArea657style={{ marginTop: "10px" }}658rows={3}659placeholder="Explain how we can test the software..."660onChange={(e) =>661update(6622,663e.target.value664? "\n\nHOW CAN WE TEST THAT THE SOFTWARE IS PROPERLY INSTALLED?\n\n" +665e.target.value666: "",667)668}669/>670</div>671);672}673function Instructions() {674return (675<div>676<p>677If the above links don't help you solve your problem, please create a678support ticket below. Support is currently available in{" "}679<b>English, German, and Russian</b> only.680</p>681</div>682);683}684685function ChatGPT({ siteName }) {686return (687<div style={{ margin: "15px 0 20px 0" }}>688<Title level={2}>ChatGPT</Title>689<div style={{ color: "#666" }}>690If you have a question about how to do something using {siteName},691ChatGPT might save you some time:692</div>693<ChatGPTHelp style={{ marginTop: "15px" }} tag={"support"} />694</div>695);696}697698function FAQ() {699return (700<div>701<Title level={2}>Helpful Links</Title>702<Alert703message={""}704style={{ margin: "20px 0" }}705type="warning"706description={707<ul style={{ marginBottom: 0, fontSize: "11pt" }}>708<li>709<A href="https://doc.cocalc.com/">The CoCalc Manual</A>710</li>711<li>712<A href="https://github.com/sagemathinc/cocalc/issues">713Bug reports714</A>715</li>716<li>717<A href="https://github.com/sagemathinc/cocalc/discussions">718The CoCalc Discussion Forum719</A>720</li>721<li>722{" "}723<A href="https://doc.cocalc.com/howto/missing-project.html">724Help: My file or project appears to be missing!725</A>{" "}726</li>727<li>728{" "}729I have{" "}730<A href="https://doc.cocalc.com/howto/sage-question.html">731general questions about SageMath...732</A>733</li>734</ul>735}736/>737</div>738);739}740741function Status({ done }) {742return (743<Icon744style={{745color: done ? "green" : "red",746fontWeight: "bold",747fontSize: "12pt",748}}749name={done ? "check" : "arrow-right"}750/>751);752}753754755