Skip to content

Commit 01f75ad

Browse files
committed
[Docs Site] Add Support AI
1 parent 02ab4da commit 01f75ad

File tree

7 files changed

+204
-1
lines changed

7 files changed

+204
-1
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@iarna/toml": "2.2.5",
4444
"@lottiefiles/dotlottie-react": "0.13.5",
4545
"@marsidev/react-turnstile": "1.1.0",
46+
"@microsoft/fetch-event-source": "2.0.1",
4647
"@nanostores/react": "1.0.0",
4748
"@octokit/webhooks-types": "7.6.1",
4849
"@stoplight/json-schema-tree": "4.0.0",
@@ -77,6 +78,7 @@
7778
"hastscript": "9.0.1",
7879
"he": "1.2.0",
7980
"jsonc-parser": "3.3.1",
81+
"ldrs": "1.1.7",
8082
"lz-string": "1.5.0",
8183
"marked": "15.0.12",
8284
"mdast-util-from-markdown": "2.0.2",

src/components/SupportAI.tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { useState } from "react";
2+
import Markdown from "react-markdown";
3+
import { fetchEventSource } from "@microsoft/fetch-event-source";
4+
import { Ring } from "ldrs/react";
5+
import "ldrs/react/Ring.css";
6+
7+
type Messages = { role: "user" | "assistant"; content: string }[];
8+
type Sources = { title: string; file_path: string }[];
9+
10+
function Messages({
11+
messages,
12+
loading,
13+
}: {
14+
messages: Messages;
15+
loading: boolean;
16+
}) {
17+
const classes = {
18+
base: "w-fit max-w-3/4 rounded p-4",
19+
user: "bg-cl1-brand-orange text-cl1-black self-end",
20+
assistant: "self-start bg-(--sl-color-bg-nav)",
21+
};
22+
23+
return (
24+
<div className="flex flex-col justify-center gap-4">
25+
{messages
26+
.filter((message) => Boolean(message.content))
27+
.map((message, index) => (
28+
<div
29+
key={index}
30+
className={`${classes.base} ${message.role === "user" ? classes.user : classes.assistant}`}
31+
>
32+
<Markdown>{message.content}</Markdown>
33+
</div>
34+
))}
35+
{loading && (
36+
<div className={`${classes.base} ${classes.assistant}`}>
37+
<Ring size={16} speed={1} color="var(--color-cl1-brand-orange)" />
38+
</div>
39+
)}
40+
</div>
41+
);
42+
}
43+
44+
export default function SupportAI() {
45+
const [threadId, setThreadId] = useState<string | undefined>();
46+
const [question, setQuestion] = useState<string>("");
47+
const [loading, setLoading] = useState<boolean>(false);
48+
49+
const [messages, setMessages] = useState<Messages>([]);
50+
51+
async function handleSubmit() {
52+
setLoading(true);
53+
setMessages((messages) => [
54+
...messages,
55+
{ role: "user", content: question },
56+
{ role: "assistant", content: "" },
57+
]);
58+
setQuestion("");
59+
60+
const controller = new AbortController();
61+
const { signal } = controller;
62+
63+
let chunkedAnswer = "";
64+
let sources: Sources = [];
65+
66+
await fetchEventSource(
67+
// "http://localhost:8010/proxy/devdocs/ask",
68+
"https://support-ai.cloudflaresupport.workers.dev/devdocs/ask",
69+
{
70+
method: "POST",
71+
body: JSON.stringify({
72+
question,
73+
threadId,
74+
}),
75+
signal,
76+
openWhenHidden: true,
77+
async onopen(response) {
78+
if (!response.ok) {
79+
throw new Error(response.status.toString());
80+
}
81+
82+
return;
83+
},
84+
onerror(error) {
85+
if (error instanceof Error) {
86+
setLoading(false);
87+
setMessages((messages) => [
88+
...messages,
89+
{
90+
role: "assistant",
91+
content:
92+
"I'm unable to provide an answer to that at the moment. Please rephrase your query and I'll try again.",
93+
},
94+
]);
95+
throw error;
96+
}
97+
},
98+
onmessage(ev) {
99+
if (ev.data === "[DONE]") {
100+
controller.abort();
101+
102+
setMessages((messages) => {
103+
const newMessages = [...messages];
104+
newMessages[newMessages.length - 1].content += [
105+
"\n\n",
106+
"I used these sources to answer your question, please review them if you need more information:",
107+
"\n\n",
108+
sources
109+
.map((source) => `- [${source.title}](${source.file_path})`)
110+
.join("\n"),
111+
].join("\n");
112+
return newMessages;
113+
});
114+
}
115+
116+
const { threadId, response, botResponse } = JSON.parse(ev.data);
117+
118+
if (botResponse?.sources) {
119+
sources = botResponse.sources;
120+
}
121+
122+
if (threadId) {
123+
setThreadId(threadId);
124+
}
125+
126+
if (!response) return;
127+
128+
chunkedAnswer += response;
129+
130+
setLoading(false);
131+
setMessages((messages) => {
132+
const newMessages = [...messages];
133+
newMessages[newMessages.length - 1].content = chunkedAnswer;
134+
return newMessages;
135+
});
136+
},
137+
},
138+
);
139+
}
140+
141+
return (
142+
<div>
143+
<Messages messages={messages} loading={loading} />
144+
<div className="flex items-center justify-center gap-4">
145+
<input
146+
className="w-full rounded p-2"
147+
type="text"
148+
placeholder="Ask a question..."
149+
value={question}
150+
disabled={loading}
151+
onChange={(e) => setQuestion(e.target.value)}
152+
onKeyDown={async (e) => {
153+
if (e.key === "Enter" && !loading) {
154+
e.preventDefault();
155+
await handleSubmit();
156+
}
157+
}}
158+
/>
159+
</div>
160+
<p className="text-center text-xs">
161+
Use of Support AI is subject to the Cloudflare Website and Online
162+
Services{" "}
163+
<a href="https://www.cloudflare.com/website-terms/">Terms of Use</a>.
164+
You acknowledge and agree that the output generated by Support AI has
165+
not been verified by Cloudflare for accuracy and does not represent
166+
Cloudflare's views.
167+
</p>
168+
</div>
169+
);
170+
}

src/content/docs/support/ai.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: Support AI
3+
tableOfContents: false
4+
sidebar:
5+
order: 8
6+
---
7+
8+
import SupportAI from "~/components/SupportAI.tsx";
9+
10+
<SupportAI client:load />

src/content/docs/support/cloudflare-status.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
pcx_content_type: concept
33
title: Cloudflare Status
4+
sidebar:
5+
order: 5
46
---
57

68
Cloudflare provides updates on the status of our services and network at https://www.cloudflarestatus.com/, which you should check if you notice unexpected behavior with Cloudflare.

src/content/docs/support/customer-incident-management-policy.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
pcx_content_type: troubleshooting
33
source: https://support.cloudflare.com/hc/en-us/articles/230054288-Customer-Incident-Management-Policy
44
title: Customer Incident Management Policy
5-
5+
sidebar:
6+
order: 6
67
---
78

89
## Purpose

src/content/docs/support/disruptive-maintenance.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
pcx_content_type: troubleshooting
33
source: https://support.cloudflare.com/hc/en-us/articles/360060050511-Disruptive-Maintenance-Windows
44
title: Disruptive Maintenance
5+
sidebar:
6+
order: 7
57
---
68

79
import { AvailableNotifications, Render } from "~/components";

0 commit comments

Comments
 (0)