From 01f75ada9a5c1276f8c99fceb929a5fc3f954a7e Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Thu, 29 May 2025 18:23:43 +0100 Subject: [PATCH 1/8] [Docs Site] Add Support AI --- package-lock.json | 16 ++ package.json | 2 + src/components/SupportAI.tsx | 170 ++++++++++++++++++ src/content/docs/support/ai.mdx | 10 ++ .../docs/support/cloudflare-status.mdx | 2 + .../customer-incident-management-policy.mdx | 3 +- .../docs/support/disruptive-maintenance.mdx | 2 + 7 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/components/SupportAI.tsx create mode 100644 src/content/docs/support/ai.mdx diff --git a/package-lock.json b/package-lock.json index 2a23f618b58458..1328e996081281 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,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", @@ -59,6 +60,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", @@ -4425,6 +4427,13 @@ "langium": "3.3.1" } }, + "node_modules/@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", + "dev": true, + "license": "MIT" + }, "node_modules/@nanostores/react": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.0.0.tgz", @@ -13817,6 +13826,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ldrs": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/ldrs/-/ldrs-1.1.7.tgz", + "integrity": "sha512-rZnfveeY1SeS3F3ifUVd9AVGTFHmQ0qzp5fuszAirnrVkjqJBLrm99vtr/Mxbby4XgadUYv+DsFqyk2p4FV40Q==", + "dev": true, + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 459b6ed352875e..3df66fe5a0c07a 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/components/SupportAI.tsx b/src/components/SupportAI.tsx new file mode 100644 index 00000000000000..2c969d6322c065 --- /dev/null +++ b/src/components/SupportAI.tsx @@ -0,0 +1,170 @@ +import { useState } from "react"; +import Markdown from "react-markdown"; +import { fetchEventSource } from "@microsoft/fetch-event-source"; +import { Ring } from "ldrs/react"; +import "ldrs/react/Ring.css"; + +type Messages = { role: "user" | "assistant"; content: string }[]; +type Sources = { title: string; file_path: string }[]; + +function Messages({ + messages, + loading, +}: { + messages: Messages; + loading: boolean; +}) { + 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)", + }; + + return ( +
+ {messages + .filter((message) => Boolean(message.content)) + .map((message, index) => ( +
+ {message.content} +
+ ))} + {loading && ( +
+ +
+ )} +
+ ); +} + +export default function SupportAI() { + const [threadId, setThreadId] = useState(); + const [question, setQuestion] = useState(""); + const [loading, setLoading] = useState(false); + + const [messages, setMessages] = useState([]); + + 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: Sources = []; + + 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.", + }, + ]); + throw error; + } + }, + onmessage(ev) { + if (ev.data === "[DONE]") { + controller.abort(); + + setMessages((messages) => { + const newMessages = [...messages]; + newMessages[newMessages.length - 1].content += [ + "\n\n", + "I used these sources to answer your question, please review them if you need more information:", + "\n\n", + sources + .map((source) => `- [${source.title}](${source.file_path})`) + .join("\n"), + ].join("\n"); + return newMessages; + }); + } + + const { threadId, response, botResponse } = JSON.parse(ev.data); + + 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 ( +
+ +
+ setQuestion(e.target.value)} + onKeyDown={async (e) => { + if (e.key === "Enter" && !loading) { + e.preventDefault(); + await handleSubmit(); + } + }} + /> +
+

+ Use of Support AI is subject to the Cloudflare Website and Online + Services{" "} + Terms of Use. + You acknowledge and agree that the output generated by Support AI has + not been verified by Cloudflare for accuracy and does not represent + Cloudflare's views. +

+
+ ); +} diff --git a/src/content/docs/support/ai.mdx b/src/content/docs/support/ai.mdx new file mode 100644 index 00000000000000..673a32598e4e11 --- /dev/null +++ b/src/content/docs/support/ai.mdx @@ -0,0 +1,10 @@ +--- +title: Support AI +tableOfContents: false +sidebar: + order: 8 +--- + +import SupportAI from "~/components/SupportAI.tsx"; + + diff --git a/src/content/docs/support/cloudflare-status.mdx b/src/content/docs/support/cloudflare-status.mdx index 29a88e575c47f0..327a55ea309a01 100644 --- a/src/content/docs/support/cloudflare-status.mdx +++ b/src/content/docs/support/cloudflare-status.mdx @@ -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. diff --git a/src/content/docs/support/customer-incident-management-policy.mdx b/src/content/docs/support/customer-incident-management-policy.mdx index efefaf245d249f..9a42f9e1423f62 100644 --- a/src/content/docs/support/customer-incident-management-policy.mdx +++ b/src/content/docs/support/customer-incident-management-policy.mdx @@ -2,7 +2,8 @@ pcx_content_type: troubleshooting source: https://support.cloudflare.com/hc/en-us/articles/230054288-Customer-Incident-Management-Policy title: Customer Incident Management Policy - +sidebar: + order: 6 --- ## Purpose diff --git a/src/content/docs/support/disruptive-maintenance.mdx b/src/content/docs/support/disruptive-maintenance.mdx index 608906008432db..c5d4b3e69400bf 100644 --- a/src/content/docs/support/disruptive-maintenance.mdx +++ b/src/content/docs/support/disruptive-maintenance.mdx @@ -2,6 +2,8 @@ pcx_content_type: troubleshooting source: https://support.cloudflare.com/hc/en-us/articles/360060050511-Disruptive-Maintenance-Windows title: Disruptive Maintenance +sidebar: + order: 7 --- import { AvailableNotifications, Render } from "~/components"; From 8f3a6775b046358223b5973435a840884afe1303 Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Mon, 2 Jun 2025 16:23:59 +0100 Subject: [PATCH 2/8] support gfm --- src/components/SupportAI.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SupportAI.tsx b/src/components/SupportAI.tsx index 2c969d6322c065..79d1dcc5bee99f 100644 --- a/src/components/SupportAI.tsx +++ b/src/components/SupportAI.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { fetchEventSource } from "@microsoft/fetch-event-source"; import { Ring } from "ldrs/react"; import "ldrs/react/Ring.css"; @@ -29,7 +30,7 @@ function Messages({ key={index} className={`${classes.base} ${message.role === "user" ? classes.user : classes.assistant}`} > - {message.content} + {message.content} ))} {loading && ( From e0323d55f03775d7206e4e9e242ea38dd31f09fb Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Mon, 2 Jun 2025 16:36:52 +0100 Subject: [PATCH 3/8] turn \n into linebreaks --- package-lock.json | 32 ++++++++++++++++++++++++++++++++ package.json | 1 + src/components/SupportAI.tsx | 11 ++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1328e996081281..edec0708336fb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,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", @@ -14598,6 +14599,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", @@ -17665,6 +17681,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-directive": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", diff --git a/package.json b/package.json index 3df66fe5a0c07a..1d4d31b4d7fcdd 100644 --- a/package.json +++ b/package.json @@ -110,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", diff --git a/src/components/SupportAI.tsx b/src/components/SupportAI.tsx index 79d1dcc5bee99f..dfbad35eeb837e 100644 --- a/src/components/SupportAI.tsx +++ b/src/components/SupportAI.tsx @@ -1,5 +1,6 @@ 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"; @@ -30,7 +31,9 @@ function Messages({ key={index} className={`${classes.base} ${message.role === "user" ? classes.user : classes.assistant}`} > - {message.content} + + {message.content} + ))} {loading && ( @@ -65,8 +68,8 @@ export default function SupportAI() { let sources: Sources = []; await fetchEventSource( - // "http://localhost:8010/proxy/devdocs/ask", - "https://support-ai.cloudflaresupport.workers.dev/devdocs/ask", + "http://localhost:8010/proxy/devdocs/ask", + // "https://support-ai.cloudflaresupport.workers.dev/devdocs/ask", { method: "POST", body: JSON.stringify({ @@ -110,6 +113,8 @@ export default function SupportAI() { .map((source) => `- [${source.title}](${source.file_path})`) .join("\n"), ].join("\n"); + + console.log(JSON.stringify(newMessages, null, 2)); return newMessages; }); } From cfaa7f89661a6dcb143a045325354e382369e4b1 Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Mon, 2 Jun 2025 16:37:34 +0100 Subject: [PATCH 4/8] remove debug --- src/components/SupportAI.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/SupportAI.tsx b/src/components/SupportAI.tsx index dfbad35eeb837e..a1610db92db578 100644 --- a/src/components/SupportAI.tsx +++ b/src/components/SupportAI.tsx @@ -68,8 +68,8 @@ export default function SupportAI() { let sources: Sources = []; await fetchEventSource( - "http://localhost:8010/proxy/devdocs/ask", - // "https://support-ai.cloudflaresupport.workers.dev/devdocs/ask", + // "http://localhost:8010/proxy/devdocs/ask", + "https://support-ai.cloudflaresupport.workers.dev/devdocs/ask", { method: "POST", body: JSON.stringify({ @@ -114,7 +114,6 @@ export default function SupportAI() { .join("\n"), ].join("\n"); - console.log(JSON.stringify(newMessages, null, 2)); return newMessages; }); } From 20a396c936272a472b146a1f22e13c45c06c46fa Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Wed, 4 Jun 2025 16:11:01 +0100 Subject: [PATCH 5/8] feedback --- src/components/SupportAI.tsx | 86 +++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/src/components/SupportAI.tsx b/src/components/SupportAI.tsx index a1610db92db578..e23b2c5cb985b9 100644 --- a/src/components/SupportAI.tsx +++ b/src/components/SupportAI.tsx @@ -4,11 +4,33 @@ 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 }[]; +type Messages = { + role: "user" | "assistant"; + content: string; + queryId?: string; +}[]; type 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, @@ -16,24 +38,56 @@ function Messages({ messages: Messages; loading: boolean; }) { + const [feedbackGiven, setFeedbackGiven] = useState>(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 (
{messages .filter((message) => Boolean(message.content)) .map((message, index) => ( -
- - {message.content} - +
+
+ + {message.content} + + {message.role === "assistant" && message.queryId && ( +
+ {feedbackGiven.has(message.queryId) ? ( + Thanks for your feedback! + ) : ( + <> + + + + )} +
+ )} +
))} {loading && ( @@ -66,6 +120,7 @@ export default function SupportAI() { let chunkedAnswer = ""; let sources: Sources = []; + let currentQueryId: string | undefined; await fetchEventSource( // "http://localhost:8010/proxy/devdocs/ask", @@ -105,7 +160,8 @@ export default function SupportAI() { setMessages((messages) => { const newMessages = [...messages]; - newMessages[newMessages.length - 1].content += [ + const lastMessage = newMessages[newMessages.length - 1]; + lastMessage.content += [ "\n\n", "I used these sources to answer your question, please review them if you need more information:", "\n\n", @@ -114,11 +170,21 @@ export default function SupportAI() { .join("\n"), ].join("\n"); + if (currentQueryId) { + lastMessage.queryId = currentQueryId; + } + return newMessages; }); } - const { threadId, response, botResponse } = JSON.parse(ev.data); + const { threadId, response, queryId, botResponse } = JSON.parse( + ev.data, + ); + + if (queryId) { + currentQueryId = queryId; + } if (botResponse?.sources) { sources = botResponse.sources; From 33ac5994b9754dba0a68be938f479adc530a179e Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Wed, 4 Jun 2025 16:30:29 +0100 Subject: [PATCH 6/8] pointer and beta --- src/components/SupportAI.tsx | 6 +++--- src/content/docs/support/ai.mdx | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/SupportAI.tsx b/src/components/SupportAI.tsx index e23b2c5cb985b9..94554e5d2571c9 100644 --- a/src/components/SupportAI.tsx +++ b/src/components/SupportAI.tsx @@ -64,21 +64,21 @@ function Messages({ {message.content} {message.role === "assistant" && message.queryId && ( -
+
{feedbackGiven.has(message.queryId) ? ( Thanks for your feedback! ) : ( <>
diff --git a/src/content/docs/support/ai.mdx b/src/content/docs/support/ai.mdx index 2b98bc0e80eee2..a601618ac22dbd 100644 --- a/src/content/docs/support/ai.mdx +++ b/src/content/docs/support/ai.mdx @@ -1,12 +1,12 @@ --- -title: Support AI (Beta) +title: Docs AI (Beta) tableOfContents: false sidebar: order: 8 - label: Support AI + label: Docs AI badge: Beta --- -import SupportAI from "~/components/SupportAI.tsx"; +import DocsAI from "~/components/DocsAI.tsx"; - + From 691d9a2a181a2ccf5978b081b73ab4bb4dfd752b Mon Sep 17 00:00:00 2001 From: Kian Newman-Hazel Date: Tue, 10 Jun 2025 20:59:49 +0100 Subject: [PATCH 8/8] use textarea --- src/components/DocsAI.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/DocsAI.tsx b/src/components/DocsAI.tsx index cb9dc6fe6c8e33..f949f0923f7e4a 100644 --- a/src/components/DocsAI.tsx +++ b/src/components/DocsAI.tsx @@ -226,15 +226,14 @@ export default function SupportAI() {
- setQuestion(e.target.value)} onKeyDown={async (e) => { - if (e.key === "Enter" && !loading) { + if (e.key === "Enter" && !e.shiftKey && !loading) { e.preventDefault(); await handleSubmit(); }