Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@iarna/toml": "2.2.5",
"@lottiefiles/dotlottie-react": "0.13.5",
"@marsidev/react-turnstile": "1.1.0",
"@microsoft/fetch-event-source": "2.0.1",
"@nanostores/react": "1.0.0",
"@octokit/webhooks-types": "7.6.1",
"@stoplight/json-schema-tree": "4.0.0",
Expand Down Expand Up @@ -77,6 +78,7 @@
"hastscript": "9.0.1",
"he": "1.2.0",
"jsonc-parser": "3.3.1",
"ldrs": "1.1.7",
"lz-string": "1.5.0",
"marked": "15.0.12",
"mdast-util-from-markdown": "2.0.2",
Expand Down Expand Up @@ -108,6 +110,7 @@
"rehype-stringify": "10.0.1",
"rehype-title-figure": "0.1.2",
"remark": "15.0.1",
"remark-breaks": "4.0.0",
"remark-gfm": "4.0.1",
"remark-stringify": "11.0.0",
"sharp": "0.34.2",
Expand Down
252 changes: 252 additions & 0 deletions src/components/DocsAI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { useState } from "react";
import Markdown from "react-markdown";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { Ring } from "ldrs/react";
import { MdOutlineThumbUp, MdOutlineThumbDown } from "react-icons/md";
import "ldrs/react/Ring.css";

type Messages = {
role: "user" | "assistant";
content: string;
queryId?: string;
sources?: { title: string; file_path: string }[];
}[];

async function sendCSATFeedback(queryId: string, positive: boolean) {
try {
await fetch("https://support-ai.cloudflaresupport.workers.dev/csat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
queryId,
positive,
}),
});
} catch (error) {
console.error("Failed to send CSAT feedback:", error);
}
}

function Messages({
messages,
loading,
}: {
messages: Messages;
loading: boolean;
}) {
const [feedbackGiven, setFeedbackGiven] = useState<Set<string>>(new Set());

const classes = {
base: "w-fit max-w-3/4 rounded p-4",
user: "bg-cl1-brand-orange text-cl1-black self-end",
assistant: "self-start bg-(--sl-color-bg-nav)",
};

const handleFeedback = async (queryId: string, positive: boolean) => {
await sendCSATFeedback(queryId, positive);
setFeedbackGiven((prev) => new Set(prev).add(queryId));
};

return (
<div className="flex flex-col justify-center gap-4">
{messages
.filter((message) => Boolean(message.content))
.map((message, index) => (
<div key={index} className="flex flex-col gap-2">
<div
className={`${classes.base} ${message.role === "user" ? classes.user : classes.assistant}`}
>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>
{message.content}
</Markdown>
{message.sources && (
<>
<p>
I used these sources to answer your question, please review
them if you need more information:
</p>
<ul>
{message.sources.map((source) => (
<li>
<a href={source.file_path} target="_blank">
{source.title}
</a>
</li>
))}
</ul>
</>
)}
{message.role === "assistant" && message.queryId && (
<div className="not-content flex gap-2 self-start">
{feedbackGiven.has(message.queryId) ? (
<span>Thanks for your feedback!</span>
) : (
<>
<button
onClick={() => handleFeedback(message.queryId!, true)}
className="cursor-pointer rounded bg-transparent p-2"
title="Thumbs up"
>
<MdOutlineThumbUp className="size-6 hover:text-green-600" />
</button>
<button
onClick={() => handleFeedback(message.queryId!, false)}
className="cursor-pointer rounded bg-transparent p-2"
title="Thumbs down"
>
<MdOutlineThumbDown className="size-6 hover:text-red-600" />
</button>
</>
)}
</div>
)}
</div>
</div>
))}
{loading && (
<div className={`${classes.base} ${classes.assistant}`}>
<Ring size={16} speed={1} color="var(--color-cl1-brand-orange)" />
</div>
)}
</div>
);
}

export default function SupportAI() {
const [threadId, setThreadId] = useState<string | undefined>();
const [question, setQuestion] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);

const [messages, setMessages] = useState<Messages>([]);

async function handleSubmit() {
setLoading(true);
setMessages((messages) => [
...messages,
{ role: "user", content: question },
{ role: "assistant", content: "" },
]);
setQuestion("");

const controller = new AbortController();
const { signal } = controller;

let chunkedAnswer = "";
let sources: Messages[number]["sources"] = [];
let currentQueryId: string | undefined;

await fetchEventSource(
// "http://localhost:8010/proxy/devdocs/ask",
"https://support-ai.cloudflaresupport.workers.dev/devdocs/ask",
{
method: "POST",
body: JSON.stringify({
question,
threadId,
}),
signal,
openWhenHidden: true,
async onopen(response) {
if (!response.ok) {
throw new Error(response.status.toString());
}

return;
},
onerror(error) {
if (error instanceof Error) {
setLoading(false);
setMessages((messages) => [
...messages,
{
role: "assistant",
content:
"I'm unable to provide an answer to that at the moment. Please rephrase your query and I'll try again.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"I'm unable to provide an answer to that at the moment. Please rephrase your query and I'll try again.",
"I'm unable to provide an answer to that at the moment. Please rephrase your query and I'll try again.\n\nIf that doesn't help, try searching [our docs](/search/) or the [Cloudflare Community](community.cloudflare.com/search/).",

},
]);
throw error;
}
},
onmessage(ev) {
if (ev.data === "[DONE]") {
controller.abort();

setMessages((messages) => {
const newMessages = [...messages];
const lastMessage = newMessages[newMessages.length - 1];

if (sources) {
lastMessage.sources = sources;
}

if (currentQueryId) {
lastMessage.queryId = currentQueryId;
}

return newMessages;
});
}

const { threadId, response, queryId, botResponse } = JSON.parse(
ev.data,
);

if (queryId) {
currentQueryId = queryId;
}

if (botResponse?.sources) {
sources = botResponse.sources;
}

if (threadId) {
setThreadId(threadId);
}

if (!response) return;

chunkedAnswer += response;

setLoading(false);
setMessages((messages) => {
const newMessages = [...messages];
newMessages[newMessages.length - 1].content = chunkedAnswer;
return newMessages;
});
},
},
);
}

return (
<div>
<Messages messages={messages} loading={loading} />
<div className="flex items-center justify-center gap-4">
<textarea
className="w-full rounded p-2"
placeholder="Ask a question..."
value={question}
disabled={loading}
onChange={(e) => setQuestion(e.target.value)}
onKeyDown={async (e) => {
if (e.key === "Enter" && !e.shiftKey && !loading) {
e.preventDefault();
await handleSubmit();
}
}}
/>
</div>
<p className="text-center text-xs">
Use of Docs AI is subject to the Cloudflare Website and Online Services{" "}
<a href="https://www.cloudflare.com/website-terms/">Terms of Use</a>.
You acknowledge and agree that the output generated by Docs AI has not
been verified by Cloudflare for accuracy and does not represent
Cloudflare's views.
</p>
</div>
);
}
12 changes: 12 additions & 0 deletions src/content/docs/support/ai.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Docs AI (Beta)
tableOfContents: false
sidebar:
order: 8
label: Docs AI
badge: Beta
---

import DocsAI from "~/components/DocsAI.tsx";

<DocsAI client:load />
2 changes: 2 additions & 0 deletions src/content/docs/support/cloudflare-status.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
pcx_content_type: concept
title: Cloudflare Status
sidebar:
order: 5
---

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.
Expand Down
Loading