From 051ac78e0e83c7bbf5d2a17f66956a9f9cb490b1 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 22 Feb 2024 22:39:47 +0100 Subject: [PATCH 1/7] feat(ai-help): show error for off-topic questions --- client/src/plus/ai-help/index.tsx | 57 +++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index f09b77f8fd5b..7e264896c6a2 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -615,6 +615,13 @@ export function AIHelpInner() { submit(question, chatId, parentId, messageId); }, [lastUserQuestion, submit]); + const isOffTopic = (message: Message) => { + return ( + message.role === MessageRole.Assistant && + message.content?.startsWith("I'm sorry, but I can't") + ); + }; + return ( <> @@ -640,26 +647,40 @@ export function AIHelpInner() { "ai-help-message", `role-${message.role}`, `status-${message.status}`, - ].join(" ")} + ] + .filter(Boolean) + .join(" ")} > -
- -
- {message.role === "user" ? ( - + {isOffTopic(message) ? ( + +

Error

+

+ AI Help cannot answer questions outside of web + development. +

+
) : ( - + <> +
+ +
+ {message.role === "user" ? ( + + ) : ( + + )} + )} ); From 1ab33fe9e38722747e59b41c7a616e992728874f Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 23 Feb 2024 14:07:25 +0100 Subject: [PATCH 2/7] refactor(ai-help): extract AIHelpAssistantResponseSources --- client/src/plus/ai-help/index.tsx | 68 +++++++++++++++++++------------ 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index 7e264896c6a2..5d7b99e5f71d 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -290,33 +290,7 @@ function AIHelpAssistantResponse({ return ( <> -
- {message.status === MessageStatus.Pending - ? MESSAGE_SEARCHING - : MESSAGE_SEARCHED} -
- {message.sources && message.sources.length > 0 && ( -
    - {message.sources.map(({ url, title }, index) => ( -
  • - gleanClick(`${AI_HELP}: link source -> ${url}`)} - target="_blank" - > - {title} - -
  • - ))} -
- )} + {(message.content || message.status === MessageStatus.InProgress || message.status === MessageStatus.Errored) && ( @@ -515,6 +489,46 @@ function AIHelpAssistantResponse({ ); } +function AIHelpAssistantResponseSources({ + message, +}: { + message: Pick; +}) { + const gleanClick = useGleanClick(); + + return ( + <> +
+ {message.status === MessageStatus.Pending + ? MESSAGE_SEARCHING + : MESSAGE_SEARCHED} +
+ {message.sources && message.sources.length > 0 && ( +
    + {message.sources.map(({ url, title }, index) => ( +
  • + gleanClick(`${AI_HELP}: link source -> ${url}`)} + target="_blank" + > + {title} + +
  • + ))} +
+ )} + + ); +} + export function AIHelpInner() { const formRef = useRef(null); const inputRef = useRef(null); From bddeb590c3c2f82e51d10125d8e3fdf4ef9aefa3 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 23 Feb 2024 14:13:49 +0100 Subject: [PATCH 3/7] feat(ai-help): show canned answer without sources for off-topic question --- client/src/plus/ai-help/constants.tsx | 4 +- client/src/plus/ai-help/index.tsx | 376 +++++++++++++------------- 2 files changed, 191 insertions(+), 189 deletions(-) diff --git a/client/src/plus/ai-help/constants.tsx b/client/src/plus/ai-help/constants.tsx index eb7e133f832b..7922d6c19529 100644 --- a/client/src/plus/ai-help/constants.tsx +++ b/client/src/plus/ai-help/constants.tsx @@ -1,6 +1,6 @@ -export const SORRY_BACKEND = "Sorry, I don't know how to help with that."; +export const SORRY_BACKEND_PREFIX = "I'm sorry, but I can't"; export const SORRY_FRONTEND = - "Sorry, I don’t know how to help with that.\n\nPlease keep in mind that I am only limited to answer based on the MDN documentation."; + "I'm sorry, but I can't answer questions outside web development."; export const MESSAGE_SEARCHING = "Searching for MDN content…"; export const MESSAGE_SEARCHED = "Consulted MDN content:"; diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index 5d7b99e5f71d..95ce61cf04a0 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -48,7 +48,7 @@ import { useUIStatus } from "../../ui-context"; import { QueueEntry } from "../../types/playground"; import { AIHelpLanding } from "./landing"; import { - SORRY_BACKEND, + SORRY_BACKEND_PREFIX, SORRY_FRONTEND, MESSAGE_SEARCHING, MESSAGE_ANSWERING, @@ -288,9 +288,13 @@ function AIHelpAssistantResponse({ let sample = 0; + const isOffTopic = + message.role === MessageRole.Assistant && + message.content?.startsWith(SORRY_BACKEND_PREFIX); + return ( <> - + {!isOffTopic && } {(message.content || message.status === MessageStatus.InProgress || message.status === MessageStatus.Errored) && ( @@ -320,153 +324,163 @@ function AIHelpAssistantResponse({ .filter(Boolean) .join(" ")} > - { - if (props.href?.startsWith("https://developer.mozilla.org/")) { - props.href = props.href.replace( - "https://developer.mozilla.org", - "" - ); - } + {isOffTopic ? ( + <>{SORRY_FRONTEND} + ) : ( + { + if ( + props.href?.startsWith("https://developer.mozilla.org/") + ) { + props.href = props.href.replace( + "https://developer.mozilla.org", + "" + ); + } - const isExternal = isExternalUrl(props.href ?? ""); + const isExternal = isExternalUrl(props.href ?? ""); - if (isExternal) { - props.className = "external"; - props.rel = "noopener noreferrer"; - } + if (isExternal) { + props.className = "external"; + props.rel = "noopener noreferrer"; + } - // Measure. - props.onClick = () => - gleanClick( - `${AI_HELP}: link ${ - isExternal ? "external" : "internal" - } -> ${props.href}` - ); + // Measure. + props.onClick = () => + gleanClick( + `${AI_HELP}: link ${ + isExternal ? "external" : "internal" + } -> ${props.href}` + ); - // Always open in new tab. - props.target = "_blank"; + // Always open in new tab. + props.target = "_blank"; - // eslint-disable-next-line jsx-a11y/anchor-has-content - return ; - }, - pre: ({ node, className, children, ...props }) => { - const code = Children.toArray(children) - .map( - (child) => - /language-(\w+)/.exec( - (child as ReactElement)?.props?.className || "" - )?.[1] - ) - .find(Boolean); + // eslint-disable-next-line jsx-a11y/anchor-has-content + return ; + }, + pre: ({ node, className, children, ...props }) => { + const code = Children.toArray(children) + .map( + (child) => + /language-(\w+)/.exec( + (child as ReactElement)?.props?.className || "" + )?.[1] + ) + .find(Boolean); - if (!code) { + if (!code) { + return ( +
+                        {children}
+                      
+ ); + } + const key = sample; + const id = `${message.messageId}--${key}`; + const isQueued = queuedExamples.has(id); + sample += 1; return ( -
+                    
+
+ {code} + {message.status === MessageStatus.Complete && + ["html", "js", "javascript", "css"].includes( + code.toLowerCase() + ) && ( +
+ { + gleanClick( + `${AI_HELP}: example ${ + isQueued ? "dequeue" : "queue" + } -> ${id}` + ); + setQueue((old) => + !old.some((item) => item.id === id) + ? [...old, createQueueEntry(id)].sort( + (a, b) => a.key - b.key + ) + : [...old].filter( + (item) => item.id !== id + ) + ); + }} + id={id} + /> + + +
+ )} +
+
{children}
+
+ ); + }, + code: ({ className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ""); + const lang = Prism.languages[match?.[1]]; + return lang ? ( + + ) : ( + {children} -
+ ); - } - const key = sample; - const id = `${message.messageId}--${key}`; - const isQueued = queuedExamples.has(id); - sample += 1; - return ( -
-
- {code} - {message.status === MessageStatus.Complete && - ["html", "js", "javascript", "css"].includes( - code.toLowerCase() - ) && ( -
- { - gleanClick( - `${AI_HELP}: example ${ - isQueued ? "dequeue" : "queue" - } -> ${id}` - ); - setQueue((old) => - !old.some((item) => item.id === id) - ? [...old, createQueueEntry(id)].sort( - (a, b) => a.key - b.key - ) - : [...old].filter((item) => item.id !== id) - ); - }} - id={id} - /> - - -
- )} -
-
{children}
-
- ); - }, - code: ({ className, children, ...props }) => { - const match = /language-(\w+)/.exec(className || ""); - const lang = Prism.languages[match?.[1]]; - return lang ? ( - - ) : ( - - {children} - - ); - }, - }} - > - {message.content?.replace(SORRY_BACKEND, SORRY_FRONTEND)} -
- {message.status === "complete" && - !message.content?.includes(SORRY_BACKEND) && ( - <> -
+ }, + }} + > + {message.content} + + )} + {(message.status === "complete" || isOffTopic) && ( + <> +
+ {!isOffTopic && ( - - Report an issue with this answer on GitHub - -
- - )} + )} + + Report an issue with this answer on GitHub + +
+ + )} )} @@ -629,13 +648,6 @@ export function AIHelpInner() { submit(question, chatId, parentId, messageId); }, [lastUserQuestion, submit]); - const isOffTopic = (message: Message) => { - return ( - message.role === MessageRole.Assistant && - message.content?.startsWith("I'm sorry, but I can't") - ); - }; - return ( <> @@ -665,37 +677,27 @@ export function AIHelpInner() { .filter(Boolean) .join(" ")} > - {isOffTopic(message) ? ( - -

Error

-

- AI Help cannot answer questions outside of web - development. -

-
- ) : ( - <> -
- -
- {message.role === "user" ? ( - - ) : ( - - )} - - )} + <> +
+ +
+ {message.role === "user" ? ( + + ) : ( + + )} + ); })} From 4660eb75a9a8c35b5050a3e0a70fc4faa9ae33bd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 1 Mar 2024 09:28:08 +0100 Subject: [PATCH 4/7] fix(ai-help): override content/sources only for off-topic messages --- client/src/plus/ai-help/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index 96a60bbe5c8e..aee5b9b30505 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -515,8 +515,12 @@ function AIHelpAssistantResponse({ messages={messages} currentMessage={{ ...message, - content: SORRY_FRONTEND, - sources: [], + ...(isOffTopic + ? { + content: SORRY_FRONTEND, + sources: [], + } + : {}), }} > Report an issue with this answer on GitHub From f03d8b6ca3e9ae08d8618a52698fe9fc4a8213bc Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 1 Mar 2024 09:30:22 +0100 Subject: [PATCH 5/7] chore(ai-help): remove unnecessary filter --- client/src/plus/ai-help/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index aee5b9b30505..b943458acbd8 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -700,9 +700,7 @@ export function AIHelpInner() { "ai-help-message", `role-${message.role}`, `status-${message.status}`, - ] - .filter(Boolean) - .join(" ")} + ].join(" ")} > <>
From a6194f34d96f14a7a0503abac1e10545c7c26c2e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 1 Mar 2024 09:32:49 +0100 Subject: [PATCH 6/7] chore(ai-help): remove unnecessary wrapper --- client/src/plus/ai-help/index.tsx | 42 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index b943458acbd8..fce85742e45c 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -702,28 +702,26 @@ export function AIHelpInner() { `status-${message.status}`, ].join(" ")} > - <> -
- -
- {message.role === "user" ? ( - - ) : ( - - )} - +
+ +
+ {message.role === "user" ? ( + + ) : ( + + )} ); })} From 706471d2ebcbaa370000a50731d3501b9fe79ebf Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 1 Mar 2024 09:35:09 +0100 Subject: [PATCH 7/7] refactor(ai-help): reuse ReactMarkdown for off-topic answer for simplicity --- client/src/plus/ai-help/index.tsx | 278 ++++++++++++++---------------- 1 file changed, 134 insertions(+), 144 deletions(-) diff --git a/client/src/plus/ai-help/index.tsx b/client/src/plus/ai-help/index.tsx index fce85742e45c..2c1943b92c51 100644 --- a/client/src/plus/ai-help/index.tsx +++ b/client/src/plus/ai-help/index.tsx @@ -346,159 +346,149 @@ function AIHelpAssistantResponse({ .filter(Boolean) .join(" ")} > - {isOffTopic ? ( - <>{SORRY_FRONTEND} - ) : ( - { - if ( - props.href?.startsWith("https://developer.mozilla.org/") - ) { - props.href = props.href.replace( - "https://developer.mozilla.org", - "" - ); - } + { + if (props.href?.startsWith("https://developer.mozilla.org/")) { + props.href = props.href.replace( + "https://developer.mozilla.org", + "" + ); + } - const isExternal = isExternalUrl(props.href ?? ""); + const isExternal = isExternalUrl(props.href ?? ""); - if (isExternal) { - props.className = "external"; - props.rel = "noopener noreferrer"; - } + if (isExternal) { + props.className = "external"; + props.rel = "noopener noreferrer"; + } - // Measure. - props.onClick = () => - gleanClick( - `${AI_HELP}: link ${ - isExternal ? "external" : "internal" - } -> ${props.href}` - ); + // Measure. + props.onClick = () => + gleanClick( + `${AI_HELP}: link ${ + isExternal ? "external" : "internal" + } -> ${props.href}` + ); - // Always open in new tab. - props.target = "_blank"; + // Always open in new tab. + props.target = "_blank"; - // eslint-disable-next-line jsx-a11y/anchor-has-content - return
; - }, - pre: ({ node, className, children, ...props }) => { - const code = Children.toArray(children) - .map( - (child) => - /language-(\w+)/.exec( - (child as ReactElement)?.props?.className || "" - )?.[1] - ) - .find(Boolean); + // eslint-disable-next-line jsx-a11y/anchor-has-content + return ; + }, + pre: ({ node, className, children, ...props }) => { + const code = Children.toArray(children) + .map( + (child) => + /language-(\w+)/.exec( + (child as ReactElement)?.props?.className || "" + )?.[1] + ) + .find(Boolean); - if (!code) { - return ( -
-                        {children}
-                      
- ); - } - const key = sample; - const id = `${message.messageId}--${key}`; - const isQueued = queuedExamples.has(id); - sample += 1; + if (!code) { return ( -
-
- {code} - {message.status === MessageStatus.Complete && - ["html", "js", "javascript", "css"].includes( - code.toLowerCase() - ) && ( -
- { - gleanClick( - `${AI_HELP}: example ${ - isQueued ? "dequeue" : "queue" - } -> ${id}` - ); - setQueue((old) => - !old.some((item) => item.id === id) - ? [...old, createQueueEntry(id)].sort( - (a, b) => a.key - b.key - ) - : [...old].filter( - (item) => item.id !== id - ) - ); - }} - id={id} - /> - - -
- )} -
-
{children}
-
- ); - }, - code: ({ className, children, ...props }) => { - const match = /language-(\w+)/.exec(className || ""); - const lang = Prism.languages[match?.[1]]; - return lang ? ( - - ) : ( - +
                       {children}
-                    
+                    
); - }, - }} - > - {message.content} - - )} + } + const key = sample; + const id = `${message.messageId}--${key}`; + const isQueued = queuedExamples.has(id); + sample += 1; + return ( +
+
+ {code} + {message.status === MessageStatus.Complete && + ["html", "js", "javascript", "css"].includes( + code.toLowerCase() + ) && ( +
+ { + gleanClick( + `${AI_HELP}: example ${ + isQueued ? "dequeue" : "queue" + } -> ${id}` + ); + setQueue((old) => + !old.some((item) => item.id === id) + ? [...old, createQueueEntry(id)].sort( + (a, b) => a.key - b.key + ) + : [...old].filter((item) => item.id !== id) + ); + }} + id={id} + /> + + +
+ )} +
+
{children}
+
+ ); + }, + code: ({ className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ""); + const lang = Prism.languages[match?.[1]]; + return lang ? ( + + ) : ( + + {children} + + ); + }, + }} + > + {isOffTopic ? SORRY_FRONTEND : message.content} + {(message.status === "complete" || isOffTopic) && ( <>