From 2eb462df849b2dc594aa729bf923b465eb75d965 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Wed, 1 May 2024 16:37:21 +0200 Subject: [PATCH 1/5] feedback buttons --- app/components/Article/Feedback.tsx | 70 +++++++++++++ app/components/Article/article.css | 4 + app/components/Article/index.tsx | 54 +--------- app/components/Chatbot/ChatEntry.tsx | 139 ++++++++++++++++---------- app/components/Chatbot/chat_entry.css | 10 +- app/components/Chatbot/index.tsx | 2 +- app/hooks/useChat.ts | 1 + app/routes/questions.actions.tsx | 6 +- 8 files changed, 178 insertions(+), 108 deletions(-) create mode 100644 app/components/Article/Feedback.tsx diff --git a/app/components/Article/Feedback.tsx b/app/components/Article/Feedback.tsx new file mode 100644 index 00000000..0b336f29 --- /dev/null +++ b/app/components/Article/Feedback.tsx @@ -0,0 +1,70 @@ +import React, {useState} from 'react' +import {CompositeButton} from '~/components/Button' +import {Action, ActionType} from '~/routes/questions.actions' +import './article.css' +import FeedbackForm from '~/components/Article/FeedbackForm' + +type FeedbackProps = { + pageid: string + showForm?: boolean + labels?: boolean + upHint?: string + downHint?: string +} +const Feedback = ({pageid, showForm, labels, upHint, downHint}: FeedbackProps) => { + const [showFeedback, setShowFeedback] = useState(false) + const [showFeedbackForm, setShowFeedbackForm] = useState(false) + const [isFormFocused, setIsFormFocused] = useState(false) + + React.useEffect(() => { + // Hide the form after 10 seconds if the user hasn't interacted with it + const timeoutId = setInterval(() => { + if (!isFormFocused) { + setShowFeedbackForm(false) + } + }, 10000) + + // Clear the timeout to prevent it from running if the component unmounts + return () => clearInterval(timeoutId) + }, [showFeedbackForm, isFormFocused]) + + React.useEffect(() => { + const timeout = setInterval(() => setShowFeedback(false), 6000) + return () => clearInterval(timeout) + }, [showFeedback]) + + return ( + + setShowFeedback(true)} + /> + setShowFeedbackForm(!!showForm)} + /> +
+ Thanks for your feedback! +
+ { + setShowFeedback(true) + setShowFeedbackForm(false) + }} + onBlur={() => setIsFormFocused(false)} + onFocus={() => setIsFormFocused(true)} + hasOptions={false} + /> +
+ ) +} + +export default Feedback diff --git a/app/components/Article/article.css b/app/components/Article/article.css index 10d06fd4..f8b06d4c 100644 --- a/app/components/Article/article.css +++ b/app/components/Article/article.css @@ -175,6 +175,10 @@ article a.see-more.visible:after { content: 'See less'; } +.feedback { + width: fit-content; +} + @media only screen and (max-width: 780px) { article { max-width: 100%; diff --git a/app/components/Article/index.tsx b/app/components/Article/index.tsx index 55a87627..08dd1fdc 100644 --- a/app/components/Article/index.tsx +++ b/app/components/Article/index.tsx @@ -1,22 +1,18 @@ -import React, {useState} from 'react' +import {useState} from 'react' import {Link} from '@remix-run/react' import KeepGoing from '~/components/Article/KeepGoing' import CopyIcon from '~/components/icons-generated/Copy' import EditIcon from '~/components/icons-generated/Pencil' import Button, {CompositeButton} from '~/components/Button' -import {Action, ActionType} from '~/routes/questions.actions' import type {Glossary, Question} from '~/server-utils/stampy' import {tagUrl} from '~/routesMapper' import Contents from './Contents' +import Feedback from './Feedback' import './article.css' -import FeedbackForm from '~/components/Article/FeedbackForm' const isLoading = ({text}: Question) => !text || text === 'Loading...' const ArticleFooter = (question: Question) => { - const [showFeedback, setShowFeedback] = useState(false) - const [showFeedbackForm, setShowFeedbackForm] = useState(false) - const [isFormFocused, setIsFormFocused] = useState(false) const date = question.updatedAt && new Date(question.updatedAt).toLocaleDateString('en-GB', { @@ -24,23 +20,6 @@ const ArticleFooter = (question: Question) => { month: 'short', }) - React.useEffect(() => { - // Hide the form after 10 seconds if the user hasn't interacted with it - const timeoutId = setInterval(() => { - if (!isFormFocused) { - setShowFeedbackForm(false) - } - }, 10000) - - // Clear the timeout to prevent it from running if the component unmounts - return () => clearInterval(timeoutId) - }, [showFeedbackForm, isFormFocused]) - - React.useEffect(() => { - const timeout = setInterval(() => setShowFeedback(false), 6000) - return () => clearInterval(timeout) - }, [showFeedback]) - return ( !isLoading(question) && (
@@ -57,34 +36,7 @@ const ArticleFooter = (question: Question) => {
Was this page helpful? - - setShowFeedback(true)} - /> - setShowFeedbackForm(true)} - /> -
- Thanks for your feedback! -
- { - setShowFeedback(true) - setShowFeedbackForm(false) - }} - onBlur={() => setIsFormFocused(false)} - onFocus={() => setIsFormFocused(true)} - hasOptions={false} - /> -
+ ) ) diff --git a/app/components/Chatbot/ChatEntry.tsx b/app/components/Chatbot/ChatEntry.tsx index bebefc51..5020a42a 100644 --- a/app/components/Chatbot/ChatEntry.tsx +++ b/app/components/Chatbot/ChatEntry.tsx @@ -10,16 +10,18 @@ import Contents from '~/components/Article/Contents' import useGlossary from '~/hooks/useGlossary' import './chat_entry.css' import type {Entry, AssistantEntry, StampyEntry, Citation, ErrorMessage} from '~/hooks/useChat' +import Feedback from '../Article/Feedback' const MAX_REFERENCES = 10 -const hints = { - bot: 'bla bla bla something bot', - human: 'bla bla bla by humans', - error: null, -} -const AnswerInfo = ({answerType}: {answerType?: 'human' | 'bot' | 'error'}) => { - if (!answerType || !hints[answerType]) return null +const AnswerInfo = ({ + answerType, + hint, +}: { + hint?: string + answerType?: 'human' | 'bot' | 'error' +}) => { + if (!answerType || !hint) return null return ( {answerType === 'human' ? : } @@ -27,7 +29,7 @@ const AnswerInfo = ({answerType}: {answerType?: 'human' | 'bot' | 'error'}) => { {answerType === 'human' ? 'Human-written' : 'Bot-generated'} response -
{hints[answerType]}
+
{hint}
) } @@ -36,12 +38,13 @@ type TitleProps = { title: string Icon: ComponentType answerType?: 'human' | 'bot' | 'error' + hint?: string } -const Title = ({title, Icon, answerType}: TitleProps) => ( +const Title = ({title, Icon, answerType, hint}: TitleProps) => (
{title} - +
) @@ -52,29 +55,13 @@ const UserQuery = ({content}: Entry) => ( ) -const md = new MarkdownIt({html: true}) -const ReferenceLink = ({id, index, text}: Citation) => { - if (!index || index > MAX_REFERENCES) return '' - - const parsed = text?.match(/^###.*?###\s+"""(.*?)"""$/ms) - return ( - <> - - {index} - - {parsed && ( -
- )} - - ) -} - -const Reference = ({id, title, authors, source, url, index}: Citation) => { +const ReferenceSummary = ({ + title, + authors, + source, + url, + titleClass, +}: Citation & {titleClass?: string}) => { const referenceSources = { arxiv: 'Scientific paper', blogs: 'Blogpost', @@ -98,23 +85,60 @@ const Reference = ({id, title, authors, source, url, index}: Citation) => { } return ( -
-
{index}
+
+
{title}
-
{title}
-
- - {' · '} - - {referenceSources[source as keyof typeof referenceSources] || new URL(url).host}{' '} - - -
+ + {' · '} + + {referenceSources[source as keyof typeof referenceSources] || new URL(url).host}{' '} + +
) } +const md = new MarkdownIt({html: true}) +const ReferencePopup = (citation: Citation) => { + const parsed = citation.text?.match(/^###.*?###\s+"""(.*?)"""$/ms) + if (!parsed) return undefined + return ( +
+ +
Referenced excerpt
+
+
+ ) +} + +const ReferenceLink = (citation: Citation) => { + const {id, index} = citation + if (!index || index > MAX_REFERENCES) return '' + + return ( + <> + + {index} + + + + ) +} + +const Reference = (citation: Citation) => { + return ( +
+
{citation.index}
+ +
+ ) +} + const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => { const citations = [] as Citation[] citationsMap?.forEach((v) => { @@ -145,7 +169,7 @@ const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => { return (
- + <Title title="Stampy" Icon={StampyIcon} answerType="bot" hint="Generated by an AI model" /> <PhaseState /> <div className="padding-bottom-24"> {content?.split(/(\[\d+\])|(\n)/).map((chunk, i) => { @@ -171,24 +195,37 @@ const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => { ) } -const StampyArticle = ({pageid, content}: StampyEntry) => { +const StampyArticle = ({pageid, content, title}: StampyEntry) => { const glossary = useGlossary() + const hint = `This response is pulled from our article "${title}" which was written by members of AISafety.info` return ( <div> - <Title title="Stampy" Icon={StampyIcon} answerType="human" /> - <article className="stampy"> - <Contents pageid={pageid || ''} html={content || 'Loading...'} glossary={glossary || {}} /> - </article> + <Title title="Stampy" Icon={StampyIcon} answerType="human" hint={hint} /> + <div className="answer"> + <article className="stampy"> + <Contents + pageid={pageid || ''} + html={content || 'Loading...'} + glossary={glossary || {}} + /> + </article> + <Feedback + pageid={pageid} + upHint="This response was helpful" + downHint="This response was unhelpful" + /> + </div> </div> ) } const ErrorReply = ({content}: ErrorMessage) => { + console.error(content) return ( <div> <Title title="Error" Icon={StampyIcon} answerType="error" /> - <div>{content}</div> + <div>Sorry, something has gone wrong. Please ask your question again!</div> </div> ) } diff --git a/app/components/Chatbot/chat_entry.css b/app/components/Chatbot/chat_entry.css index 4d40fbe1..de86300b 100644 --- a/app/components/Chatbot/chat_entry.css +++ b/app/components/Chatbot/chat_entry.css @@ -24,9 +24,12 @@ article.stampy { visibility: hidden; } +.chat-entry .hint-contents:hover, .chat-entry .hint:hover + .hint-contents { visibility: visible; - background: red; + background: var(--colors-cool-grey-800); + color: white; + padding: var(--spacing-16); } .chat-entry .reference-link { @@ -112,8 +115,9 @@ article.stampy { transition: visibility 0.2s; max-width: 600px; word-wrap: break-word; - background-color: var(--colors-cool-grey-300); - padding: var(--spacing-16) var(--spacing-24); + background-color: white; + border: 1px solid var(--colors-cool-grey-200, #dfe3e9); + padding: var(--spacing-24) var(--spacing-32); position: absolute; transform: translateX(50%); text-decoration: unset; diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx index 168e93b3..cee0ded3 100644 --- a/app/components/Chatbot/index.tsx +++ b/app/components/Chatbot/index.tsx @@ -169,7 +169,7 @@ export const Chatbot = ({question, questions, settings}: ChatbotProps) => { setFollowups( question.relatedQuestions?.slice(0, 3).map(({title, pageid}) => ({text: title, pageid})) ) - return {...item, content: question.text || ''} + return {...item, title: question.title, content: question.text || ''} } // this is a previous human written article that didn't load properly - don't // update the text as that could cause things to jump around - the user has diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts index b2836e44..b30dc452 100644 --- a/app/hooks/useChat.ts +++ b/app/hooks/useChat.ts @@ -49,6 +49,7 @@ export type StampyEntry = { pageid: string content: string deleted?: boolean + title?: string } export type Followup = { diff --git a/app/routes/questions.actions.tsx b/app/routes/questions.actions.tsx index d28b0068..b08f5a92 100644 --- a/app/routes/questions.actions.tsx +++ b/app/routes/questions.actions.tsx @@ -90,6 +90,7 @@ type Props = { pageid: string actionType: ActionType showText?: boolean + hint?: string children?: ReactNode | ReactNode[] [k: string]: unknown onSuccess?: () => void @@ -99,6 +100,7 @@ export const Action = ({ pageid, actionType, showText = true, + hint, children, onSuccess, onClick, @@ -162,7 +164,7 @@ export const Action = ({ replace action="/questions/actions" method="post" - title={title} + title={hint || title} onClick={handleAction} {...props} > @@ -175,7 +177,7 @@ export const Action = ({ <Icon /> <p className={[actionTaken ? 'teal-500' : 'grey', 'small'].join(' ')}> {' '} - {showText && title} + {showText && (hint || title)} </p> </Button> </Form> From ef44a89bca5cd914e2ae0c49101ee369053ff5a7 Mon Sep 17 00:00:00 2001 From: Melissa Samworth <melissasamworth@gmail.com> Date: Thu, 2 May 2024 17:04:04 -0400 Subject: [PATCH 2/5] new input component, CSS --- app/assets/icons/link-out.svg | 4 + app/assets/icons/stampy.svg | 4 +- app/components/Button/button.css | 110 ++++++++++++++++++--- app/components/Chatbot/ChatEntry.tsx | 2 +- app/components/Chatbot/chat_entry.css | 40 ++++---- app/components/Chatbot/index.tsx | 38 ++++--- app/components/Chatbot/widgit.css | 26 ++++- app/components/Input/index.tsx | 33 +++++++ app/components/Input/input.css | 48 +++++++++ app/components/icons-generated/LinkOut.tsx | 14 +++ app/components/icons-generated/Stampy.tsx | 4 +- app/components/icons-generated/index.ts | 1 + app/hooks/useChat.ts | 3 +- app/root.css | 48 +++++++++ 14 files changed, 319 insertions(+), 56 deletions(-) create mode 100644 app/assets/icons/link-out.svg create mode 100644 app/components/Input/index.tsx create mode 100644 app/components/Input/input.css create mode 100644 app/components/icons-generated/LinkOut.tsx diff --git a/app/assets/icons/link-out.svg b/app/assets/icons/link-out.svg new file mode 100644 index 00000000..1d790672 --- /dev/null +++ b/app/assets/icons/link-out.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.63636 2C9.33512 2 9.09091 2.24421 9.09091 2.54545C9.09091 2.8467 9.33512 3.09091 9.63636 3.09091H12.1378L7.61439 7.61431C7.40137 7.82732 7.40137 8.17268 7.61439 8.38569C7.8274 8.59871 8.17276 8.59871 8.38578 8.38569L12.9091 3.86238V6.36364C12.9091 6.66488 13.1533 6.90909 13.4545 6.90909C13.7558 6.90909 14 6.66488 14 6.36364V2.54545C14 2.24421 13.7558 2 13.4545 2H9.63636Z" fill="#1D9089"/> +<path d="M12.3636 8C12.6649 8 12.9091 8.24421 12.9091 8.54545V13.4545C12.9091 13.7558 12.6649 14 12.3636 14H2.54545C2.24421 14 2 13.7558 2 13.4545V3.63636C2 3.33512 2.24421 3.09091 2.54545 3.09091H7.45455C7.75579 3.09091 8 3.33512 8 3.63636C8 3.93761 7.75579 4.18182 7.45455 4.18182H3.09091V12.9091H11.8182V8.54545C11.8182 8.24421 12.0624 8 12.3636 8Z" fill="#1D9089"/> +</svg> diff --git a/app/assets/icons/stampy.svg b/app/assets/icons/stampy.svg index 6aa06153..260b18c5 100644 --- a/app/assets/icons/stampy.svg +++ b/app/assets/icons/stampy.svg @@ -1,4 +1,4 @@ -<svg width="40" height="35" viewBox="0 0 40 35" fill="none" xmlns="http://www.w3.org/2000/svg"> +<svg width="80" height="69" viewBox="0 0 80 69" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_2209_1647)"> <path opacity="0.5" d="M38.6239 31.104C37.5999 31.104 36.7679 30.272 36.7679 29.248C36.7679 28.224 37.5999 27.392 38.6239 27.392C38.6879 27.392 38.7519 27.392 38.8479 27.392V25.12C38.7839 25.12 38.7199 25.12 38.6239 25.12C37.5999 25.12 36.7679 24.288 36.7679 23.264C36.7679 22.24 37.5999 21.408 38.6239 21.408C38.6879 21.408 38.7519 21.408 38.8479 21.408V19.136C38.7839 19.136 38.7199 19.136 38.6239 19.136C37.5999 19.136 36.7679 18.304 36.7679 17.28C36.7679 16.256 37.5999 15.424 38.6239 15.424C38.6879 15.424 38.7519 15.424 38.8479 15.424V13.152C38.2399 13.216 37.6319 12.992 37.2159 12.512C36.5439 11.712 36.6399 10.56 37.4399 9.88798C37.8559 9.53598 38.3679 9.40798 38.8479 9.47198V7.19998C38.7839 7.19998 38.7199 7.19998 38.6239 7.19998C37.5999 7.19998 36.7679 6.36798 36.7679 5.34398C36.7679 4.31998 37.5999 3.48798 38.6239 3.48798C38.6879 3.48798 38.7519 3.48798 38.8479 3.48798V0.703979H36.6399C36.6399 1.72798 35.8079 2.55998 34.7839 2.55998C33.7599 2.55998 32.9279 1.72798 32.9279 0.703979H30.5279C30.5279 1.72798 29.6959 2.55998 28.6719 2.55998C27.6479 2.55998 26.8159 1.72798 26.8159 0.703979H24.4159C24.4159 1.72798 23.5839 2.55998 22.5599 2.55998C21.5359 2.55998 20.7039 1.72798 20.7039 0.703979H18.3039C18.3039 1.72798 17.4719 2.55998 16.4479 2.55998C15.4239 2.55998 14.5919 1.72798 14.5919 0.703979H12.1919C12.1919 1.72798 11.3599 2.55998 10.3359 2.55998C9.31191 2.55998 8.47991 1.72798 8.47991 0.703979H6.07991C6.07991 1.72798 5.24791 2.55998 4.22391 2.55998C3.19991 2.55998 2.36791 1.72798 2.36791 0.703979H0.159912V3.48798C1.18391 3.48798 2.01591 4.31998 2.01591 5.34398C2.01591 6.36798 1.18391 7.19998 0.159912 7.19998V9.43998C0.703912 9.43998 1.21591 9.66398 1.59991 10.112C2.27191 10.912 2.17591 12.064 1.37591 12.736C1.02391 13.024 0.607912 13.184 0.159912 13.184V15.424C1.18391 15.424 2.01591 16.256 2.01591 17.28C2.01591 18.304 1.18391 19.136 0.159912 19.136V21.376C1.18391 21.376 2.01591 22.208 2.01591 23.232C2.01591 24.256 1.18391 25.088 0.159912 25.088V27.328C1.18391 27.328 2.01591 28.16 2.01591 29.184C2.01591 30.208 1.18391 31.04 0.159912 31.04V33.824H2.36791C2.36791 32.8 3.19991 31.968 4.22391 31.968C5.24791 31.968 6.07991 32.8 6.07991 33.824H8.47991C8.47991 32.8 9.31191 31.968 10.3359 31.968C11.3599 31.968 12.1919 32.8 12.1919 33.824H14.5919C14.5919 32.8 15.4239 31.968 16.4479 31.968C17.4719 31.968 18.3039 32.8 18.3039 33.824H20.7039C20.7039 32.8 21.5359 31.968 22.5599 31.968C23.5839 31.968 24.4159 32.8 24.4159 33.824H26.8159C26.8159 32.8 27.6479 31.968 28.6719 31.968C29.6959 31.968 30.5279 32.8 30.5279 33.824H32.9279C32.9279 32.8 33.7599 31.968 34.7839 31.968C35.8079 31.968 36.6399 32.8 36.6399 33.824H38.8479V31.04C38.7519 31.104 38.6879 31.104 38.6239 31.104Z" fill="#ECA680"/> <path d="M39.904 34.4H36.64V33.856C36.64 33.12 36.032 32.512 35.296 32.512C34.56 32.512 33.952 33.12 33.952 33.856V34.4H30.496V33.856C30.496 33.12 29.888 32.512 29.152 32.512C28.416 32.512 27.808 33.12 27.808 33.856V34.4H24.352V33.856C24.352 33.12 23.744 32.512 23.008 32.512C22.272 32.512 21.664 33.12 21.664 33.856V34.4H18.272V33.856C18.272 33.12 17.664 32.512 16.928 32.512C16.192 32.512 15.584 33.12 15.584 33.856V34.4H12.128V33.856C12.128 33.12 11.52 32.512 10.784 32.512C10.048 32.512 9.43996 33.12 9.43996 33.856V34.4H6.01596V33.856C6.01596 33.12 5.40796 32.512 4.67196 32.512C3.93596 32.512 3.32797 33.12 3.32797 33.856V34.4H0.0639648V30.56H0.607964C1.34396 30.56 1.95196 29.952 1.95196 29.216C1.95196 28.48 1.34396 27.872 0.607964 27.872H0.0639648V24.576H0.607964C1.34396 24.576 1.95196 23.968 1.95196 23.232C1.95196 22.496 1.34396 21.888 0.607964 21.888H0.0639648V18.592H0.607964C1.34396 18.592 1.95196 17.984 1.95196 17.248C1.95196 16.512 1.34396 15.904 0.607964 15.904H0.0639648V12.64H0.607964C0.927964 12.64 1.21596 12.544 1.47196 12.32C1.75996 12.096 1.91996 11.776 1.95196 11.424C1.98396 11.072 1.88796 10.72 1.63196 10.464C1.37596 10.176 0.991964 9.98399 0.607964 9.98399H0.0639648V6.65599H0.607964C1.34396 6.65599 1.95196 6.04799 1.95196 5.31199C1.95196 4.57599 1.34396 3.96799 0.607964 3.96799H0.0639648V0.127991H3.32797V0.67199C3.32797 1.40799 3.93596 2.01599 4.67196 2.01599C5.40796 2.01599 6.01596 1.40799 6.01596 0.67199V0.127991H9.47196V0.67199C9.47196 1.40799 10.08 2.01599 10.816 2.01599C11.552 2.01599 12.16 1.40799 12.16 0.67199V0.127991H15.616V0.67199C15.616 1.40799 16.224 2.01599 16.96 2.01599C17.696 2.01599 18.304 1.40799 18.304 0.67199V0.127991H21.76V0.67199C21.76 1.40799 22.368 2.01599 23.104 2.01599C23.84 2.01599 24.448 1.40799 24.448 0.67199V0.127991H27.84V0.67199C27.84 1.40799 28.448 2.01599 29.184 2.01599C29.92 2.01599 30.528 1.40799 30.528 0.67199V0.127991H33.984V0.67199C33.984 1.40799 34.592 2.01599 35.328 2.01599C36.064 2.01599 36.672 1.40799 36.672 0.67199V0.127991H39.936V4.06399L39.328 3.99999C39.264 3.99999 39.232 3.99999 39.168 3.99999C38.432 3.99999 37.824 4.60799 37.824 5.34399C37.824 6.07999 38.432 6.68799 39.168 6.68799C39.2 6.68799 39.264 6.68799 39.328 6.68799L39.936 6.62399V10.08L39.328 10.016C38.944 9.98399 38.592 10.08 38.304 10.304C38.016 10.528 37.856 10.848 37.824 11.2C37.792 11.552 37.888 11.904 38.144 12.16C38.432 12.512 38.88 12.672 39.328 12.64L39.936 12.576V16.032L39.328 15.968C39.264 15.968 39.232 15.968 39.168 15.968C38.432 15.968 37.824 16.576 37.824 17.312C37.824 18.048 38.432 18.656 39.168 18.656C39.2 18.656 39.264 18.656 39.328 18.656L39.936 18.592V22.048L39.328 21.984C39.264 21.984 39.232 21.984 39.168 21.984C38.432 21.984 37.824 22.592 37.824 23.328C37.824 24.064 38.432 24.672 39.168 24.672C39.2 24.672 39.264 24.672 39.328 24.672L39.936 24.608V28.064L39.328 28C39.264 28 39.232 28 39.168 28C38.432 28 37.824 28.608 37.824 29.344C37.824 30.08 38.432 30.688 39.168 30.688C39.2 30.688 39.264 30.688 39.328 30.688L39.936 30.624V34.4H39.904ZM37.632 33.344H38.816V31.616C37.632 31.456 36.736 30.464 36.736 29.248C36.736 28.032 37.632 27.008 38.816 26.88V25.664C37.632 25.504 36.736 24.512 36.736 23.296C36.736 22.08 37.632 21.056 38.816 20.928V19.712C37.632 19.552 36.736 18.56 36.736 17.344C36.736 16.128 37.632 15.104 38.816 14.976V13.76C38.24 13.696 37.696 13.376 37.312 12.928C36.896 12.448 36.704 11.808 36.768 11.168C36.832 10.528 37.12 9.95199 37.6 9.53599C37.952 9.24799 38.368 9.05599 38.816 8.99199V7.77599C37.632 7.61599 36.736 6.62399 36.736 5.40799C36.736 4.19199 37.632 3.16799 38.816 3.03999V1.31199H37.632C37.376 2.36799 36.416 3.16799 35.296 3.16799C34.144 3.16799 33.184 2.36799 32.96 1.31199H31.52C31.264 2.36799 30.304 3.16799 29.184 3.16799C28.032 3.16799 27.072 2.36799 26.848 1.31199H25.408C25.152 2.36799 24.192 3.16799 23.072 3.16799C21.92 3.16799 20.96 2.36799 20.736 1.31199H19.296C19.04 2.36799 18.08 3.16799 16.96 3.16799C15.808 3.16799 14.848 2.36799 14.624 1.31199H13.184C12.928 2.36799 11.968 3.16799 10.848 3.16799C9.69596 3.16799 8.73596 2.36799 8.51196 1.31199H7.07196C6.81596 2.36799 5.85596 3.16799 4.73596 3.16799C3.58396 3.16799 2.62396 2.36799 2.39996 1.31199H1.21596V3.07199C2.27196 3.32799 3.07196 4.28799 3.07196 5.40799C3.07196 6.55999 2.27196 7.51999 1.21596 7.74399V9.02399C1.72796 9.15199 2.17597 9.40799 2.52797 9.82399C2.94396 10.304 3.13596 10.944 3.07196 11.584C3.00796 12.224 2.71996 12.8 2.23996 13.216C1.95196 13.472 1.59996 13.632 1.21596 13.728V15.008C2.27196 15.264 3.07196 16.224 3.07196 17.344C3.07196 18.464 2.27196 19.456 1.21596 19.68V20.96C2.27196 21.216 3.07196 22.176 3.07196 23.296C3.07196 24.416 2.27196 25.408 1.21596 25.632V26.912C2.27196 27.168 3.07196 28.128 3.07196 29.248C3.07196 30.368 2.27196 31.36 1.21596 31.584V33.344H2.39996C2.65596 32.288 3.61596 31.488 4.73596 31.488C5.88796 31.488 6.84796 32.288 7.07196 33.344H8.51196C8.76796 32.288 9.72796 31.488 10.848 31.488C12 31.488 12.96 32.288 13.184 33.344H14.624C14.88 32.288 15.84 31.488 16.96 31.488C18.112 31.488 19.072 32.288 19.296 33.344H20.736C20.992 32.288 21.952 31.488 23.072 31.488C24.224 31.488 25.184 32.288 25.408 33.344H26.848C27.104 32.288 28.064 31.488 29.184 31.488C30.336 31.488 31.296 32.288 31.52 33.344H32.96C33.216 32.288 34.176 31.488 35.296 31.488C36.448 31.456 37.408 32.256 37.632 33.344Z" fill="#ECA680"/> @@ -15,7 +15,7 @@ </g> <defs> <clipPath id="clip0_2209_1647"> -<rect width="40" height="34.56" fill="white"/> +<rect width="80" height="69" fill="white"/> </clipPath> </defs> </svg> diff --git a/app/components/Button/button.css b/app/components/Button/button.css index 67dd63a4..b7ef695f 100644 --- a/app/components/Button/button.css +++ b/app/components/Button/button.css @@ -10,54 +10,140 @@ border-radius: var(--border-radius); box-sizing: border-box; + background: var(--colors-white, #ffffff); + border: 1px solid var(--colors-cool-grey-200, #dfe3e9); + font-size: 16px; + font-style: normal; font-weight: 300; line-height: 170%; /* 27.2px */ letter-spacing: -0.16px; - - background: var(--colors-white, #ffffff); - border: 1px solid var(--colors-cool-grey-200, #dfe3e9); } +/* style */ + .primary { background: var(--colors-teal-500, #1d9089); font-weight: 500; color: var(--colors-white, #ffffff); -} -.primary:hover { - background: var(--colors-teal-700, #17736e); + font-size: 16px; + font-weight: 300; + line-height: 170%; /* 27.2px */ + letter-spacing: -0.16px; } .primary-alt { background: var(--colors-white, #ffffff); color: var(--colors-teal-500, #1d9089); -} -.primary-alt:hover { - color: var(--colors-teal-800, #146560); + font-size: 16px; + font-weight: 300; + line-height: 170%; /* 27.2px */ + letter-spacing: -0.16px; } .secondary { color: var(--colors-cool-grey-600, #788492) !important; border: 1px solid var(--colors-cool-grey-200, #dfe3e9); background: var(--colors-white, #fff); -} -.secondary:hover { - border: 1px solid var(--colors-teal-200, #a6d9d7) !important; + font-size: 16px; + font-weight: 300; + line-height: 170%; /* 27.2px */ + letter-spacing: -0.16px; } .secondary-selected { background: var(--colors-white, #ffffff); color: var(--colors-teal-500, #1d9089); border: 1px solid var(--colors-teal-500, #1d9089); + + font-size: 16px; + font-weight: 300; + line-height: 170%; /* 27.2px */ + letter-spacing: -0.16px; } .secondary-alt { color: var(--colors-teal-500, #1d9089); border: 1px solid var(--colors-cool-grey-200, #dfe3e9); background: var(--colors-white, #fff); + + font-size: 16px; + font-weight: 300; + line-height: 170%; /* 27.2px */ + letter-spacing: -0.16px; +} + +/* large */ + +.primary-large { + background: var(--colors-teal-500, #1d9089); + font-weight: 500; + color: var(--colors-white, #ffffff); + + font-size: 18px; + font-weight: 300; + line-height: 170%; /* 30.6px */ + letter-spacing: -0.18px; +} + +.primary-alt-large { + background: var(--colors-white, #ffffff); + color: var(--colors-teal-500, #1d9089); + + font-size: 18px; + font-weight: 300; + line-height: 170%; /* 30.6px */ + letter-spacing: -0.18px; +} + +.secondary-large { + color: var(--colors-cool-grey-600, #788492) !important; + border: 1px solid var(--colors-cool-grey-200, #dfe3e9); + background: var(--colors-white, #fff); + + font-size: 18px; + font-weight: 300; + line-height: 170%; /* 30.6px */ + letter-spacing: -0.18px; +} + +.secondary-selected-large { + background: var(--colors-white, #ffffff); + color: var(--colors-teal-500, #1d9089); + border: 1px solid var(--colors-teal-500, #1d9089); + + font-size: 18px; + font-weight: 300; + line-height: 170%; /* 30.6px */ + letter-spacing: -0.18px; +} + +.secondary-alt-large { + color: var(--colors-teal-500, #1d9089); + border: 1px solid var(--colors-cool-grey-200, #dfe3e9); + background: var(--colors-white, #fff); + + font-size: 18px; + font-weight: 300; + line-height: 170%; /* 30.6px */ + letter-spacing: -0.18px; +} + +/* state */ + +.primary:hover { + background: var(--colors-teal-700, #17736e); +} + +.primary-alt:hover { + color: var(--colors-teal-800, #146560); +} + +.secondary:hover { + border: 1px solid var(--colors-teal-200, #a6d9d7) !important; } .secondary-alt:hover { diff --git a/app/components/Chatbot/ChatEntry.tsx b/app/components/Chatbot/ChatEntry.tsx index bebefc51..930c3d2b 100644 --- a/app/components/Chatbot/ChatEntry.tsx +++ b/app/components/Chatbot/ChatEntry.tsx @@ -3,7 +3,7 @@ import {Link} from '@remix-run/react' import MarkdownIt from 'markdown-it' import QuestionMarkIcon from '~/components/icons-generated/QuestionMark' import BotIcon from '~/components/icons-generated/Bot' -import LinkIcon from '~/components/icons-generated/Link' +import LinkIcon from '~/components/icons-generated/LinkOut' import PersonIcon from '~/components/icons-generated/Person' import StampyIcon from '~/components/icons-generated/Stampy' import Contents from '~/components/Article/Contents' diff --git a/app/components/Chatbot/chat_entry.css b/app/components/Chatbot/chat_entry.css index 4d40fbe1..8aa9ac8a 100644 --- a/app/components/Chatbot/chat_entry.css +++ b/app/components/Chatbot/chat_entry.css @@ -43,53 +43,53 @@ article.stampy { } .ref-1 { - background: rgb(211, 255, 253); - color: rgb(24, 185, 71); + background: #dbffed; + color: #00c159; } .ref-2 { - background: rgb(255, 221, 244); - color: rgb(251, 0, 158); + background: #ffe5f6; + color: #ff16ae; } .ref-3 { - background: rgb(217, 253, 254); - color: rgb(23, 184, 197); + background: #e0fdff; + color: #00c3d0; } .ref-4 { - background: rgb(254, 230, 202); - color: rgb(230, 107, 9); + background: #ffebd4; + color: #ed8000; } .ref-5 { - background: rgb(244, 223, 255); - color: rgb(164, 3, 254); + background: #f6e7ff; + color: #b53aff; } .ref-6 { - background: rgb(231, 255, 178); - color: rgb(99, 159, 4); + background: #ebffbf; + color: #74ac00; } .ref-7 { - background: rgb(231, 232, 255); - color: rgb(77, 75, 254); + background: #ecedff; + color: #6068ff; } .ref-8 { - background: rgb(255, 254, 156); - color: rgb(144, 140, 5); + background: #fffcac; + color: #a19b00; } .ref-9 { - background: rgb(254, 226, 226); - color: rgb(200, 0, 5); + background: #ffe8e8; + color: #d50000; } .ref-10 { - background: rgb(214, 240, 255); - color: rgb(18, 144, 254); + background: #def3ff; + color: #00a4ff; } .reference { diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx index 06c8d20f..2e56000f 100644 --- a/app/components/Chatbot/index.tsx +++ b/app/components/Chatbot/index.tsx @@ -9,6 +9,7 @@ import './widgit.css' import {questionUrl} from '~/routesMapper' import {Question} from '~/server-utils/stampy' import {useSearch} from '~/hooks/useSearch' +import Input from '~/components/Input' export const WidgetStampy = () => { const [question, setQuestion] = useState('') @@ -32,7 +33,7 @@ export const WidgetStampy = () => { <div className="padding-bottom-24">Try asking me...</div> {questions.map((question, i) => ( <div key={i} className="padding-bottom-16"> - <Button className="secondary-alt" action={stampyUrl(question)}> + <Button className="secondary-alt-large" action={stampyUrl(question)}> {question} </Button> </div> @@ -68,7 +69,11 @@ type QuestionInputProps = { } const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => { const [question, setQuestion] = useState(initial || '') +<<<<<<< Updated upstream +======= + const [, setPlaceholder] = useState('Ask Stampy a question...') +>>>>>>> Stashed changes const handleAsk = (val: string) => { onAsk && onAsk(val) setQuestion('') @@ -81,10 +86,16 @@ const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => { return ( <div className="widget-ask flex-container"> +<<<<<<< Updated upstream <input type="text" className="full-width bordered secondary" placeholder="Ask Stampy a question..." +======= + <Input + placeHolder="Ask Stampy a question..." + className="large col-10" +>>>>>>> Stashed changes value={question} onChange={(e) => handleChange(e.target.value)} onKeyDown={(e) => { @@ -102,10 +113,11 @@ type FollowupsProps = { title?: string followups?: Followup[] onSelect: (followup: Followup) => void + className?: string } -const Followups = ({title, followups, onSelect}: FollowupsProps) => ( +const Followups = ({title, followups, onSelect, className}: FollowupsProps) => ( <> - {title && <div className="padding-bottom-24">{title}</div>} + {title && <div className={'padding-bottom-24 color-grey ' + (className || '')}>{title}</div>} {followups?.map(({text, pageid}, i) => ( <div key={i} className="padding-bottom-16"> @@ -125,16 +137,18 @@ const SplashScreen = ({ onQuestion: (v: string) => void }) => ( <> - <StampyIcon /> - <div className="col-6 padding-bottom-56"> - <h2 className="teal-500">Hi there, I'm Stampy.</h2> - <h2>I can answer your questions about AI safety</h2> + <div className="padding-top-40"> + <StampyIcon /> + <div className="col-6 padding-bottom-40 padding-top-40"> + <h2 className="teal-500">Hi there, I'm Stampy.</h2> + <h2>I can answer your questions about AI Safety</h2> + </div> + <Followups + title="Popular questions" + followups={questions?.map((text: string) => ({text}))} + onSelect={({text}: Followup) => onQuestion(text)} + /> </div> - <Followups - title="Popular questions" - followups={questions?.map((text: string) => ({text}))} - onSelect={({text}: Followup) => onQuestion(text)} - /> </> ) diff --git a/app/components/Chatbot/widgit.css b/app/components/Chatbot/widgit.css index 2071a918..6e6ff71a 100644 --- a/app/components/Chatbot/widgit.css +++ b/app/components/Chatbot/widgit.css @@ -10,17 +10,33 @@ background: var(--colors-cool-grey-100); } -.widget-ask input { - height: 56px; - padding: var(--spacing-8) var(--spacing-24); -} - @media (max-width: 780px) { .button { width: 100%; } } +<<<<<<< Updated upstream +======= +.widget-ask { + position: sticky; + bottom: 0; +} +.right-icon { + padding-right: var(--spacing-56); +} +.warning-floating { + position: fixed; + right: 4.44vw; + z-index: 100; + bottom: 4.44vw; + width: 10.13vw; +} +.red { + color: #d40000; +} + +>>>>>>> Stashed changes .settings-container { position: fixed; bottom: var(--spacing-32); diff --git a/app/components/Input/index.tsx b/app/components/Input/index.tsx new file mode 100644 index 00000000..069d3dd3 --- /dev/null +++ b/app/components/Input/index.tsx @@ -0,0 +1,33 @@ +import './input.css' + +type InputProps = { + className?: string + disabled?: boolean + placeHolder?: string + value?: string + onChange?: (e: any) => void + onKeyDown?: (e: any) => void +} +const Input = ({ + className, + disabled = false, + placeHolder, + value, + onChange, + onKeyDown, +}: InputProps) => { + const classes = ['input', className].filter((i) => i).join(' ') + + return ( + <input + className={classes} + disabled={disabled} + placeholder={placeHolder} + value={value} + onChange={onChange} + onKeyDown={onKeyDown} + /> + ) +} + +export default Input diff --git a/app/components/Input/input.css b/app/components/Input/input.css new file mode 100644 index 00000000..dbf4c2b8 --- /dev/null +++ b/app/components/Input/input.css @@ -0,0 +1,48 @@ +/* I define the first class, which in the component is set up to add to every component, as the default values */ +/* You could use this class and nothing else and it will still show up correctly */ +/* the class has the same title as the component */ + +.input { + border-radius: var(--border-radius); + border: 1px solid var(--colors-cool-grey-200, #dfe3e9); + background: var(--colors-white, #fff); + box-shadow: 0px 16px 40px 0px rgba(175, 183, 194, 0.2); + color: var(--colors-cool-grey-900, #1b2b3e); + padding: var(--spacing-8) 0 var(--spacing-8) var(--spacing-12); + height: var(--spacing-48); + font-size: 16px; + font-weight: 300; + line-height: 170%; /* 27.2px */ + letter-spacing: -0.16px; + transition: border 0.2s; +} + +/* I then define all properties from Figma as titles (e.g. 'size' and 'state' for input) */ +/* and all possible values as unique classes within those properties (except for 'default') */ +/* I have to make sure to do .input.large so that this doesn't get in the way of other components */ + +/* size */ + +.input.large { + height: var(--spacing-56); + font-size: 18px; + letter-spacing: -0.18px; +} + +/* states */ + +.input:hover { + border: 1px solid var(--colors-teal-200, #a6d9d7); +} + +.input:focus { + border: 1px solid var(--colors-teal-500, #1d9089) !important; + outline: none; + color: var(--colors-cool-grey-900, #1b2b3e); +} + +.input:disabled, +.input[disabled] { + opacity: 0.6; + cursor: inherit; +} diff --git a/app/components/icons-generated/LinkOut.tsx b/app/components/icons-generated/LinkOut.tsx new file mode 100644 index 00000000..85b746b9 --- /dev/null +++ b/app/components/icons-generated/LinkOut.tsx @@ -0,0 +1,14 @@ +import type {SVGProps} from 'react' +const SvgLinkOut = (props: SVGProps<SVGSVGElement>) => ( + <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}> + <path + fill="#1D9089" + d="M9.636 2a.545.545 0 0 0 0 1.09h2.502L7.614 7.615a.545.545 0 1 0 .772.772l4.523-4.524v2.502a.545.545 0 1 0 1.091 0V2.545A.545.545 0 0 0 13.454 2z" + /> + <path + fill="#1D9089" + d="M12.364 8c.3 0 .545.244.545.545v4.91a.545.545 0 0 1-.545.545H2.545A.545.545 0 0 1 2 13.454V3.637c0-.3.244-.545.545-.545h4.91a.545.545 0 0 1 0 1.09H3.09v8.728h8.727V8.545c0-.3.244-.545.546-.545" + /> + </svg> +) +export default SvgLinkOut diff --git a/app/components/icons-generated/Stampy.tsx b/app/components/icons-generated/Stampy.tsx index 48dc30f0..bf3a1720 100644 --- a/app/components/icons-generated/Stampy.tsx +++ b/app/components/icons-generated/Stampy.tsx @@ -1,6 +1,6 @@ import type {SVGProps} from 'react' const SvgStampy = (props: SVGProps<SVGSVGElement>) => ( - <svg xmlns="http://www.w3.org/2000/svg" width={40} height={35} fill="none" {...props}> + <svg xmlns="http://www.w3.org/2000/svg" width={80} height={69} fill="none" {...props}> <g clipPath="url(#stampy_svg__a)"> <path fill="#ECA680" @@ -31,7 +31,7 @@ const SvgStampy = (props: SVGProps<SVGSVGElement>) => ( </g> <defs> <clipPath id="stampy_svg__a"> - <path fill="#fff" d="M0 0h40v34.56H0z" /> + <path fill="#fff" d="M0 0h80v69H0z" /> </clipPath> </defs> </svg> diff --git a/app/components/icons-generated/index.ts b/app/components/icons-generated/index.ts index 6ad14434..7854bc06 100644 --- a/app/components/icons-generated/index.ts +++ b/app/components/icons-generated/index.ts @@ -28,6 +28,7 @@ export {default as GroupTopEcplise} from './GroupTopEcplise' export {default as Hide} from './Hide' export {default as IntroMobile} from './IntroMobile' export {default as Like} from './Like' +export {default as LinkOut} from './LinkOut' export {default as Link} from './Link' export {default as ListLarge} from './ListLarge' export {default as MagnifyingGlass} from './MagnifyingGlass' diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts index d4d6c946..b2836e44 100644 --- a/app/hooks/useChat.ts +++ b/app/hooks/useChat.ts @@ -1,5 +1,4 @@ -// export const CHATBOT_URL = 'https://chat.stampy.ai:8443/chat' -export const CHATBOT_URL = 'http://127.0.0.1:3001/chat' +export const CHATBOT_URL = 'https://chat.stampy.ai:8443/chat' export type Citation = { title: string diff --git a/app/root.css b/app/root.css index 57cc51b5..8c6b1066 100644 --- a/app/root.css +++ b/app/root.css @@ -290,6 +290,54 @@ h2 { /* width classes. please turn the grid on in figma and then define widths based on how many columns there are! */ /* note I may change these to vars later */ +.col-1 { + width: clamp(0, 3.07vw, 53px); +} + +.col-2 { + width: clamp(0, 8.54vw, 171px); +} + +.col-3 { + width: clamp(0, 15vw, 288px); +} + +.col-4 { + width: clamp(0, 21.46vw, 405px); +} + +.col-5 { + width: clamp(0, 27.99vw, 523px); +} + +.col-6 { + width: clamp(0, 34.44vw, 640px); +} + +.col-7 { + width: clamp(0, 40.9vw, 757px); +} + +.col-8 { + width: clamp(0, 47.43vw, 875px); +} + +.col-9 { + width: clamp(0, 53.89vw, 992px); +} + +.col-10 { + width: clamp(0, 60.35vw, 1109px); +} + +.col-11 { + width: clamp(0, 66.88vw, 1227px); +} + +.col-12 { + width: clamp(0, 73.33vw, 1344px); +} + .fcol-2 { flex: 2; } From 36a5481cdec117e3a6862382bb26853f895c7a67 Mon Sep 17 00:00:00 2001 From: Melissa Samworth <melissasamworth@gmail.com> Date: Thu, 2 May 2024 17:21:56 -0400 Subject: [PATCH 3/5] new input component, CSS --- app/components/Chatbot/index.tsx | 12 ------------ app/components/Chatbot/widgit.css | 4 +--- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx index 2e56000f..7c5fc697 100644 --- a/app/components/Chatbot/index.tsx +++ b/app/components/Chatbot/index.tsx @@ -69,11 +69,6 @@ type QuestionInputProps = { } const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => { const [question, setQuestion] = useState(initial || '') -<<<<<<< Updated upstream - -======= - const [, setPlaceholder] = useState('Ask Stampy a question...') ->>>>>>> Stashed changes const handleAsk = (val: string) => { onAsk && onAsk(val) setQuestion('') @@ -86,16 +81,9 @@ const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => { return ( <div className="widget-ask flex-container"> -<<<<<<< Updated upstream - <input - type="text" - className="full-width bordered secondary" - placeholder="Ask Stampy a question..." -======= <Input placeHolder="Ask Stampy a question..." className="large col-10" ->>>>>>> Stashed changes value={question} onChange={(e) => handleChange(e.target.value)} onKeyDown={(e) => { diff --git a/app/components/Chatbot/widgit.css b/app/components/Chatbot/widgit.css index 6e6ff71a..47107ae9 100644 --- a/app/components/Chatbot/widgit.css +++ b/app/components/Chatbot/widgit.css @@ -16,8 +16,6 @@ } } -<<<<<<< Updated upstream -======= .widget-ask { position: sticky; bottom: 0; @@ -32,11 +30,11 @@ bottom: 4.44vw; width: 10.13vw; } + .red { color: #d40000; } ->>>>>>> Stashed changes .settings-container { position: fixed; bottom: var(--spacing-32); From ed8d2eff8a22a60b1bc0147588ae1c26432bb746 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell <github@ahiru.pl> Date: Thu, 2 May 2024 23:51:33 +0200 Subject: [PATCH 4/5] feedback form --- app/components/Article/Feedback.tsx | 70 ------------- app/components/Article/FeedbackForm/index.tsx | 99 ------------------- app/components/Article/article.css | 4 - app/components/Article/index.tsx | 2 +- app/components/Chatbot/ChatEntry.tsx | 25 ++++- app/components/Chatbot/index.tsx | 3 +- app/components/Feedback/Form.tsx | 67 +++++++++++++ .../feedback.css} | 30 ++---- app/components/Feedback/index.tsx | 61 ++++++++++++ app/hooks/useChat.ts | 2 + app/hooks/useOnOutsideClick.ts | 25 +++++ app/root.css | 9 ++ stories/FeedbackForm.stories.tsx | 2 +- 13 files changed, 199 insertions(+), 200 deletions(-) delete mode 100644 app/components/Article/Feedback.tsx delete mode 100644 app/components/Article/FeedbackForm/index.tsx create mode 100644 app/components/Feedback/Form.tsx rename app/components/{Article/FeedbackForm/feedbackForm.css => Feedback/feedback.css} (70%) create mode 100644 app/components/Feedback/index.tsx create mode 100644 app/hooks/useOnOutsideClick.ts diff --git a/app/components/Article/Feedback.tsx b/app/components/Article/Feedback.tsx deleted file mode 100644 index 0b336f29..00000000 --- a/app/components/Article/Feedback.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, {useState} from 'react' -import {CompositeButton} from '~/components/Button' -import {Action, ActionType} from '~/routes/questions.actions' -import './article.css' -import FeedbackForm from '~/components/Article/FeedbackForm' - -type FeedbackProps = { - pageid: string - showForm?: boolean - labels?: boolean - upHint?: string - downHint?: string -} -const Feedback = ({pageid, showForm, labels, upHint, downHint}: FeedbackProps) => { - const [showFeedback, setShowFeedback] = useState(false) - const [showFeedbackForm, setShowFeedbackForm] = useState(false) - const [isFormFocused, setIsFormFocused] = useState(false) - - React.useEffect(() => { - // Hide the form after 10 seconds if the user hasn't interacted with it - const timeoutId = setInterval(() => { - if (!isFormFocused) { - setShowFeedbackForm(false) - } - }, 10000) - - // Clear the timeout to prevent it from running if the component unmounts - return () => clearInterval(timeoutId) - }, [showFeedbackForm, isFormFocused]) - - React.useEffect(() => { - const timeout = setInterval(() => setShowFeedback(false), 6000) - return () => clearInterval(timeout) - }, [showFeedback]) - - return ( - <CompositeButton className="flex-container relative feedback"> - <Action - pageid={pageid} - showText={!!labels} - actionType={ActionType.HELPFUL} - hint={upHint} - onSuccess={() => setShowFeedback(true)} - /> - <Action - pageid={pageid} - showText={!!labels} - hint={downHint} - actionType={ActionType.UNHELPFUL} - onClick={() => setShowFeedbackForm(!!showForm)} - /> - <div className={['action-feedback-text', showFeedback ? 'show' : ''].join(' ')}> - Thanks for your feedback! - </div> - <FeedbackForm - pageid={pageid} - className={['feedback-form', showFeedbackForm ? 'show' : ''].join(' ')} - onClose={() => { - setShowFeedback(true) - setShowFeedbackForm(false) - }} - onBlur={() => setIsFormFocused(false)} - onFocus={() => setIsFormFocused(true)} - hasOptions={false} - /> - </CompositeButton> - ) -} - -export default Feedback diff --git a/app/components/Article/FeedbackForm/index.tsx b/app/components/Article/FeedbackForm/index.tsx deleted file mode 100644 index 50e08416..00000000 --- a/app/components/Article/FeedbackForm/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, {ChangeEvent} from 'react' -import Button from '~/components/Button' -import './feedbackForm.css' - -export interface FeedbackFormProps { - pageid: string - className?: string - onClose?: () => void - onBlur?: () => void - onFocus?: () => void - hasOptions?: boolean -} -const FeedbackForm = ({ - pageid, - className = 'feedback-form', - onClose, - onBlur, - onFocus, - hasOptions = true, -}: FeedbackFormProps) => { - // to be implemented. - const [selected, setSelected] = React.useState<string>() - const options = [ - { - text: 'Making things up', - option: 'making_things_up', - }, - { - text: 'Being mean', - option: 'being_mean', - }, - { - text: 'Typos', - option: 'typos', - }, - ] - const [enabledSubmit, setEnabledSubmit] = React.useState(!hasOptions) - const selectFeedback = (option: string) => { - setSelected(option) - - if (onFocus) { - onFocus() - } - setEnabledSubmit(true) - } - const handleBlur = React.useCallback( - (e: ChangeEvent<HTMLElement>) => { - const currentTarget = e.currentTarget - - // Give browser time to focus the next element - requestAnimationFrame(() => { - // Check if the new focused element is a child of the original container - if (!currentTarget.contains(document.activeElement)) { - if (onBlur) { - onBlur() - } - } - }) - }, - [onBlur] - ) - - const handleSubmit = () => { - onClose && onClose() - } - - return ( - <div key={pageid} className={className} onBlur={handleBlur} onFocus={onFocus}> - <div className={'fcol-5 feedback-container bordered'}> - <span className={'black small'}>What was the problem?</span> - {hasOptions - ? options.map((option, index) => ( - <Button - key={index} - className={[ - option.text == selected ? 'secondary-alt selected' : 'secondary', - 'select-option', - ].join(' ')} - action={() => selectFeedback(option.text)} - > - <div>{option.text}</div> - </Button> - )) - : null} - - <textarea - name={'feedback-text'} - className={['feedback-text bordered', !hasOptions ? 'no-options' : ''].join(' ')} - placeholder={'Leave a comment (optional)'} - ></textarea> - <Button className="primary full-width" action={handleSubmit} disabled={!enabledSubmit}> - <p>Submit feedback</p> - </Button> - </div> - </div> - ) -} - -export default FeedbackForm diff --git a/app/components/Article/article.css b/app/components/Article/article.css index f8b06d4c..10d06fd4 100644 --- a/app/components/Article/article.css +++ b/app/components/Article/article.css @@ -175,10 +175,6 @@ article a.see-more.visible:after { content: 'See less'; } -.feedback { - width: fit-content; -} - @media only screen and (max-width: 780px) { article { max-width: 100%; diff --git a/app/components/Article/index.tsx b/app/components/Article/index.tsx index 08dd1fdc..905f51d7 100644 --- a/app/components/Article/index.tsx +++ b/app/components/Article/index.tsx @@ -4,10 +4,10 @@ import KeepGoing from '~/components/Article/KeepGoing' import CopyIcon from '~/components/icons-generated/Copy' import EditIcon from '~/components/icons-generated/Pencil' import Button, {CompositeButton} from '~/components/Button' +import Feedback from '~/components/Feedback' import type {Glossary, Question} from '~/server-utils/stampy' import {tagUrl} from '~/routesMapper' import Contents from './Contents' -import Feedback from './Feedback' import './article.css' const isLoading = ({text}: Question) => !text || text === 'Loading...' diff --git a/app/components/Chatbot/ChatEntry.tsx b/app/components/Chatbot/ChatEntry.tsx index 5020a42a..d7fbad29 100644 --- a/app/components/Chatbot/ChatEntry.tsx +++ b/app/components/Chatbot/ChatEntry.tsx @@ -7,10 +7,10 @@ import LinkIcon from '~/components/icons-generated/Link' import PersonIcon from '~/components/icons-generated/Person' import StampyIcon from '~/components/icons-generated/Stampy' import Contents from '~/components/Article/Contents' +import Feedback from '~/components/Feedback' import useGlossary from '~/hooks/useGlossary' import './chat_entry.css' import type {Entry, AssistantEntry, StampyEntry, Citation, ErrorMessage} from '~/hooks/useChat' -import Feedback from '../Article/Feedback' const MAX_REFERENCES = 10 @@ -190,6 +190,21 @@ const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => { <div className="padding-top-32">{citations?.slice(0, MAX_REFERENCES).map(Reference)}</div> </> )} + {['followups', 'done'].includes(phase || '') ? ( + <Feedback + showForm + pageid="chatbot" + upHint="This response was helpful" + downHint="This response was unhelpful" + options={[ + 'Making things up', + 'Wrong subject', + 'Confusing', + 'Issues with sources', + 'Other', + ]} + /> + ) : undefined} {phase === 'followups' ? <p>Checking for followups...</p> : undefined} </div> ) @@ -211,9 +226,17 @@ const StampyArticle = ({pageid, content, title}: StampyEntry) => { /> </article> <Feedback + showForm pageid={pageid} upHint="This response was helpful" downHint="This response was unhelpful" + options={[ + 'Making things up', + 'Wrong subject', + 'Confusing', + 'Issues with sources', + 'Other', + ]} /> </div> </div> diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx index cee0ded3..c8db850e 100644 --- a/app/components/Chatbot/index.tsx +++ b/app/components/Chatbot/index.tsx @@ -202,7 +202,8 @@ export const Chatbot = ({question, questions, settings}: ChatbotProps) => { setHistory((current) => { const last = current[current.length - 1] if ( - (last?.role === 'assistant' && ['streaming', 'followups'].includes(last?.phase || '')) || + (last?.role === 'assistant' && + ['streaming', 'followups', 'done'].includes(last?.phase || '')) || (last?.role === 'stampy' && last?.content) || ['error'].includes(last?.role) ) { diff --git a/app/components/Feedback/Form.tsx b/app/components/Feedback/Form.tsx new file mode 100644 index 00000000..c2054599 --- /dev/null +++ b/app/components/Feedback/Form.tsx @@ -0,0 +1,67 @@ +import {useEffect, useState} from 'react' +import Button from '~/components/Button' +import useOutsideOnClick from '~/hooks/useOnOutsideClick' +import './feedback.css' + +export type FeedbackFormProps = { + onClose?: () => void + options?: string[] +} +const FeedbackForm = ({onClose, options}: FeedbackFormProps) => { + const [selected, setSelected] = useState<string>() + const [enabledSubmit, setEnabledSubmit] = useState(!options) + const [numClicks, setNumClicks] = useState(0) + const clickCheckerRef = useOutsideOnClick(onClose) + + useEffect(() => { + // Hide the form after 10 seconds if the user hasn't interacted with it + const timeoutId = setInterval(() => { + onClose && onClose() + }, 10000) + + // Clear the timeout to prevent it from running if the component unmounts + return () => clearInterval(timeoutId) + }, [numClicks, onClose]) + + const selectFeedback = (option: string) => { + setSelected(option) + setEnabledSubmit(true) + } + + const handleSubmit = () => { + onClose && onClose() + } + + return ( + <div + ref={clickCheckerRef} + onClick={() => setNumClicks((current) => current + 1)} + className="fcol-5 feedback-form bordered" + > + <span className="black small">What was the problem?</span> + {options?.map((option) => ( + <Button + key={option} + className={[ + option == selected ? 'secondary-alt selected' : 'secondary', + 'select-option', + ].join(' ')} + action={() => selectFeedback(option)} + > + {option} + </Button> + ))} + + <textarea + name="feedback-text" + className={['feedback-text bordered', !options ? 'no-options' : ''].join(' ')} + placeholder="Leave a comment (optional)" + /> + <Button className="primary full-width" action={handleSubmit} disabled={!enabledSubmit}> + <p>Submit feedback</p> + </Button> + </div> + ) +} + +export default FeedbackForm diff --git a/app/components/Article/FeedbackForm/feedbackForm.css b/app/components/Feedback/feedback.css similarity index 70% rename from app/components/Article/FeedbackForm/feedbackForm.css rename to app/components/Feedback/feedback.css index 1b8a356a..67068bd9 100644 --- a/app/components/Article/FeedbackForm/feedbackForm.css +++ b/app/components/Feedback/feedback.css @@ -1,4 +1,10 @@ -.feedback-container { +.feedback .composite-button { + width: fit-content; +} + +.feedback-form { + position: relative; + z-index: 2; max-width: 384px; padding: var(--spacing-32); } @@ -13,12 +19,6 @@ border: 1px solid var(--colors-teal-500); } -.feedback-form { - position: relative; - color: var(--colors-cool-grey-600); - z-index: 2; -} - .feedback-text { width: calc(100% - var(--spacing-24)); max-width: calc(100% - var(--spacing-24)); @@ -35,24 +35,8 @@ opacity: 1; } -.tooltip:hover::after { - display: block; -} - -.action-feedback-text { - display: none; - position: absolute; - transform: translate(-1vw, var(--spacing-56)); -} -.action-feedback-text.show { - display: block; -} .composite-button > .feedback-form { position: absolute; - display: none; transform: translate(-9vw, var(--spacing-56)); margin: var(--spacing-24); } -.composite-button > .feedback-form.show { - display: block; -} diff --git a/app/components/Feedback/index.tsx b/app/components/Feedback/index.tsx new file mode 100644 index 00000000..885f3974 --- /dev/null +++ b/app/components/Feedback/index.tsx @@ -0,0 +1,61 @@ +import {useEffect, useState} from 'react' +import {CompositeButton} from '~/components/Button' +import {Action, ActionType} from '~/routes/questions.actions' +import './feedback.css' +import FeedbackForm from './Form' + +type FeedbackProps = { + pageid: string + showForm?: boolean + labels?: boolean + upHint?: string + downHint?: string + options?: string[] +} +const Feedback = ({pageid, showForm, labels, upHint, downHint, options}: FeedbackProps) => { + const [showFeedback, setShowFeedback] = useState(false) + const [showFeedbackForm, setShowFeedbackForm] = useState(false) + + useEffect(() => { + const timeout = setInterval(() => setShowFeedback(false), 6000) + return () => clearInterval(timeout) + }, [showFeedback]) + + return ( + <div className="feedback relative"> + <CompositeButton className="flex-container"> + <Action + pageid={pageid} + showText={!!labels} + actionType={ActionType.HELPFUL} + hint={upHint} + onClick={() => setShowFeedback(true)} + /> + <Action + pageid={pageid} + showText={!!labels} + hint={downHint} + actionType={ActionType.UNHELPFUL} + onClick={() => { + setShowFeedback(!showForm) + setShowFeedbackForm(!!showForm) + }} + /> + </CompositeButton> + + {showFeedback && <div className="thanks">Thanks for your feedback!</div>} + + {showFeedbackForm && ( + <FeedbackForm + onClose={() => { + setShowFeedback(true) + setShowFeedbackForm(false) + }} + options={options} + /> + )} + </div> + ) +} + +export default Feedback diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts index b30dc452..009bd398 100644 --- a/app/hooks/useChat.ts +++ b/app/hooks/useChat.ts @@ -22,6 +22,7 @@ export type ChatPhase = | 'llm' | 'streaming' | 'followups' + | 'done' export type UserEntry = { role: 'user' @@ -229,6 +230,7 @@ export const extractAnswer = async ( followups = data.followups.map((value: any) => value as Followup) break case 'done': + result = {...result, phase: 'done'} break case 'error': throw data.error diff --git a/app/hooks/useOnOutsideClick.ts b/app/hooks/useOnOutsideClick.ts new file mode 100644 index 00000000..ba545179 --- /dev/null +++ b/app/hooks/useOnOutsideClick.ts @@ -0,0 +1,25 @@ +import {useEffect, useRef} from 'react' + +const useOutsideOnClick = (onClick?: () => void) => { + const ref = useRef<HTMLDivElement>(null) + + useEffect(() => { + if (!onClick) return + + const handleClickOutside = (e: MouseEvent) => { + if (ref.current && !(ref.current as any)?.contains(e.target)) { + onClick() + } + } + + document.addEventListener('mousedown', handleClickOutside) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [ref, onClick]) + + return ref +} + +export default useOutsideOnClick diff --git a/app/root.css b/app/root.css index 5f95175b..d35ea368 100644 --- a/app/root.css +++ b/app/root.css @@ -396,6 +396,15 @@ svg { cursor: pointer; } +.hidden { + visibility: hidden; + transition: visibility 0.2s; +} +.shown { + visibility: visible; + transition-delay: 0s; +} + /* for troubleshooting */ .pink { diff --git a/stories/FeedbackForm.stories.tsx b/stories/FeedbackForm.stories.tsx index feba8c94..bb7b41e2 100644 --- a/stories/FeedbackForm.stories.tsx +++ b/stories/FeedbackForm.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react' -import FeedbackForm from '../app/components/Article/FeedbackForm' +import FeedbackForm from '../app/components/Feedback' const meta = { title: 'Components/Article/FeedbackForm', component: FeedbackForm, From ba022925e3ab42e92478e78b3d55dfe9546ad6ee Mon Sep 17 00:00:00 2001 From: Melissa Samworth <melissasamworth@gmail.com> Date: Thu, 2 May 2024 18:19:15 -0400 Subject: [PATCH 5/5] a couple old CSS changes --- app/root.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/root.css b/app/root.css index 5f95175b..0b04cf4f 100644 --- a/app/root.css +++ b/app/root.css @@ -405,7 +405,7 @@ svg { /* defines the standard left and right margins */ .page-body { - padding: 0 var(--spacing-104); + padding: 0px 13.333vw; } /* all other classes */ @@ -465,6 +465,7 @@ svg { h1 { font-size: 38px; font-weight: 500; + font-weight: 500; line-height: 130%; /* 49.4px */ letter-spacing: -0.57px; } @@ -472,6 +473,7 @@ svg { h2 { font-size: 26px; font-weight: 500; + font-weight: 500; line-height: 145%; /* 37.7px */ letter-spacing: -0.52px; } @@ -481,6 +483,7 @@ svg { .large { font-size: 18px; font-weight: 300; + font-weight: 300; line-height: 170%; /* 30.6px */ letter-spacing: -0.18px; } @@ -488,6 +491,7 @@ svg { .large-reading { font-size: 18px; font-weight: 300; + font-weight: 300; line-height: 190%; /* 34.2px */ letter-spacing: -0.18px; } @@ -495,6 +499,7 @@ svg { .large-bold { font-size: 18px; font-weight: 500; + font-weight: 500; line-height: 170%; /* 30.6px */ letter-spacing: -0.18px; }