Skip to content

Commit d830e5d

Browse files
KianNHsdnts
authored andcommitted
[Docs Site] Add DocsAI.tsx (cloudflare#22754)
1 parent d1fa163 commit d830e5d

File tree

7 files changed

+321
-1
lines changed

7 files changed

+321
-1
lines changed

package-lock.json

Lines changed: 48 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: 3 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",
@@ -108,6 +110,7 @@
108110
"rehype-stringify": "10.0.1",
109111
"rehype-title-figure": "0.1.2",
110112
"remark": "15.0.1",
113+
"remark-breaks": "4.0.0",
111114
"remark-gfm": "4.0.1",
112115
"remark-stringify": "11.0.0",
113116
"sharp": "0.34.2",

src/components/DocsAI.tsx

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { useState } from "react";
2+
import Markdown from "react-markdown";
3+
import remarkBreaks from "remark-breaks";
4+
import remarkGfm from "remark-gfm";
5+
import { fetchEventSource } from "@microsoft/fetch-event-source";
6+
import { Ring } from "ldrs/react";
7+
import { MdOutlineThumbUp, MdOutlineThumbDown } from "react-icons/md";
8+
import "ldrs/react/Ring.css";
9+
10+
type Messages = {
11+
role: "user" | "assistant";
12+
content: string;
13+
queryId?: string;
14+
sources?: { title: string; file_path: string }[];
15+
}[];
16+
17+
async function sendCSATFeedback(queryId: string, positive: boolean) {
18+
try {
19+
await fetch("https://support-ai.cloudflaresupport.workers.dev/csat", {
20+
method: "POST",
21+
headers: {
22+
"Content-Type": "application/json",
23+
},
24+
body: JSON.stringify({
25+
queryId,
26+
positive,
27+
}),
28+
});
29+
} catch (error) {
30+
console.error("Failed to send CSAT feedback:", error);
31+
}
32+
}
33+
34+
function Messages({
35+
messages,
36+
loading,
37+
}: {
38+
messages: Messages;
39+
loading: boolean;
40+
}) {
41+
const [feedbackGiven, setFeedbackGiven] = useState<Set<string>>(new Set());
42+
43+
const classes = {
44+
base: "w-fit max-w-3/4 rounded p-4",
45+
user: "bg-cl1-brand-orange text-cl1-black self-end",
46+
assistant: "self-start bg-(--sl-color-bg-nav)",
47+
};
48+
49+
const handleFeedback = async (queryId: string, positive: boolean) => {
50+
await sendCSATFeedback(queryId, positive);
51+
setFeedbackGiven((prev) => new Set(prev).add(queryId));
52+
};
53+
54+
return (
55+
<div className="flex flex-col justify-center gap-4">
56+
{messages
57+
.filter((message) => Boolean(message.content))
58+
.map((message, index) => (
59+
<div key={index} className="flex flex-col gap-2">
60+
<div
61+
className={`${classes.base} ${message.role === "user" ? classes.user : classes.assistant}`}
62+
>
63+
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>
64+
{message.content}
65+
</Markdown>
66+
{message.sources && (
67+
<>
68+
<p>
69+
I used these sources to answer your question, please review
70+
them if you need more information:
71+
</p>
72+
<ul>
73+
{message.sources.map((source) => (
74+
<li>
75+
<a href={source.file_path} target="_blank">
76+
{source.title}
77+
</a>
78+
</li>
79+
))}
80+
</ul>
81+
</>
82+
)}
83+
{message.role === "assistant" && message.queryId && (
84+
<div className="not-content flex gap-2 self-start">
85+
{feedbackGiven.has(message.queryId) ? (
86+
<span>Thanks for your feedback!</span>
87+
) : (
88+
<>
89+
<button
90+
onClick={() => handleFeedback(message.queryId!, true)}
91+
className="cursor-pointer rounded bg-transparent p-2"
92+
title="Thumbs up"
93+
>
94+
<MdOutlineThumbUp className="size-6 hover:text-green-600" />
95+
</button>
96+
<button
97+
onClick={() => handleFeedback(message.queryId!, false)}
98+
className="cursor-pointer rounded bg-transparent p-2"
99+
title="Thumbs down"
100+
>
101+
<MdOutlineThumbDown className="size-6 hover:text-red-600" />
102+
</button>
103+
</>
104+
)}
105+
</div>
106+
)}
107+
</div>
108+
</div>
109+
))}
110+
{loading && (
111+
<div className={`${classes.base} ${classes.assistant}`}>
112+
<Ring size={16} speed={1} color="var(--color-cl1-brand-orange)" />
113+
</div>
114+
)}
115+
</div>
116+
);
117+
}
118+
119+
export default function SupportAI() {
120+
const [threadId, setThreadId] = useState<string | undefined>();
121+
const [question, setQuestion] = useState<string>("");
122+
const [loading, setLoading] = useState<boolean>(false);
123+
124+
const [messages, setMessages] = useState<Messages>([]);
125+
126+
async function handleSubmit() {
127+
setLoading(true);
128+
setMessages((messages) => [
129+
...messages,
130+
{ role: "user", content: question },
131+
{ role: "assistant", content: "" },
132+
]);
133+
setQuestion("");
134+
135+
const controller = new AbortController();
136+
const { signal } = controller;
137+
138+
let chunkedAnswer = "";
139+
let sources: Messages[number]["sources"] = [];
140+
let currentQueryId: string | undefined;
141+
142+
await fetchEventSource(
143+
// "http://localhost:8010/proxy/devdocs/ask",
144+
"https://support-ai.cloudflaresupport.workers.dev/devdocs/ask",
145+
{
146+
method: "POST",
147+
body: JSON.stringify({
148+
question,
149+
threadId,
150+
}),
151+
signal,
152+
openWhenHidden: true,
153+
async onopen(response) {
154+
if (!response.ok) {
155+
throw new Error(response.status.toString());
156+
}
157+
158+
return;
159+
},
160+
onerror(error) {
161+
if (error instanceof Error) {
162+
setLoading(false);
163+
setMessages((messages) => [
164+
...messages,
165+
{
166+
role: "assistant",
167+
content:
168+
"I'm unable to provide an answer to that at the moment. Please rephrase your query and I'll try again.",
169+
},
170+
]);
171+
throw error;
172+
}
173+
},
174+
onmessage(ev) {
175+
if (ev.data === "[DONE]") {
176+
controller.abort();
177+
178+
setMessages((messages) => {
179+
const newMessages = [...messages];
180+
const lastMessage = newMessages[newMessages.length - 1];
181+
182+
if (sources) {
183+
lastMessage.sources = sources;
184+
}
185+
186+
if (currentQueryId) {
187+
lastMessage.queryId = currentQueryId;
188+
}
189+
190+
return newMessages;
191+
});
192+
}
193+
194+
const { threadId, response, queryId, botResponse } = JSON.parse(
195+
ev.data,
196+
);
197+
198+
if (queryId) {
199+
currentQueryId = queryId;
200+
}
201+
202+
if (botResponse?.sources) {
203+
sources = botResponse.sources;
204+
}
205+
206+
if (threadId) {
207+
setThreadId(threadId);
208+
}
209+
210+
if (!response) return;
211+
212+
chunkedAnswer += response;
213+
214+
setLoading(false);
215+
setMessages((messages) => {
216+
const newMessages = [...messages];
217+
newMessages[newMessages.length - 1].content = chunkedAnswer;
218+
return newMessages;
219+
});
220+
},
221+
},
222+
);
223+
}
224+
225+
return (
226+
<div>
227+
<Messages messages={messages} loading={loading} />
228+
<div className="flex items-center justify-center gap-4">
229+
<textarea
230+
className="w-full rounded p-2"
231+
placeholder="Ask a question..."
232+
value={question}
233+
disabled={loading}
234+
onChange={(e) => setQuestion(e.target.value)}
235+
onKeyDown={async (e) => {
236+
if (e.key === "Enter" && !e.shiftKey && !loading) {
237+
e.preventDefault();
238+
await handleSubmit();
239+
}
240+
}}
241+
/>
242+
</div>
243+
<p className="text-center text-xs">
244+
Use of Docs AI is subject to the Cloudflare Website and Online Services{" "}
245+
<a href="https://www.cloudflare.com/website-terms/">Terms of Use</a>.
246+
You acknowledge and agree that the output generated by Docs AI has not
247+
been verified by Cloudflare for accuracy and does not represent
248+
Cloudflare's views.
249+
</p>
250+
</div>
251+
);
252+
}

src/content/docs/support/ai.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: Docs AI (Beta)
3+
tableOfContents: false
4+
sidebar:
5+
order: 8
6+
label: Docs AI
7+
badge: Beta
8+
---
9+
10+
import DocsAI from "~/components/DocsAI.tsx";
11+
12+
<DocsAI 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.

0 commit comments

Comments
 (0)