From 595007507f4c75461e07e978eb481c7fdb1747d3 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 3 Sep 2026 15:24:39 +0530 Subject: [PATCH] (feat)widget: sleeker interface --- .../src/components/DocsGPTWidget.tsx | 1110 ++++++++++------- .../react-widget/src/requests/streamingApi.ts | 4 +- extensions/react-widget/src/types/index.ts | 14 + 3 files changed, 696 insertions(+), 432 deletions(-) diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 392f8e70..91fa16f1 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -7,6 +7,8 @@ import { RocketIcon, ExclamationTriangleIcon, Cross2Icon, + EnterFullScreenIcon, + ExitFullScreenIcon, } from '@radix-ui/react-icons'; import { FEEDBACK, @@ -26,17 +28,21 @@ import { type StreamEvent, } from '../utils/streamEvents'; -const LikeIcon = (props: React.SVGProps) => ( +type ToggleIconProps = { filled?: boolean } & React.SVGProps; + +const LikeIcon = ({ filled, ...props }: ToggleIconProps) => ( ) => ( ); -const DislikeIcon = (props: React.SVGProps) => ( +const DislikeIcon = ({ filled, ...props }: ToggleIconProps) => ( ({ + ...base, + width: 'min(880px, calc(100vw - 32px))', + height: 'min(900px, calc(100vh - 32px))', + maxWidth: 'calc(100vw - 32px)', + maxHeight: 'calc(100vh - 32px)', +}); const openContainer = keyframes` - 0% { - width: 200px; - height: 100px; - } - 100% { - width: ${(props) => props.theme.dimensions!.width}; - height: ${(props) => props.theme.dimensions!.height}; - border-radius: 12px; - }`; + from { + width: 200px; + height: 100px; + } +`; const closeContainer = keyframes` - 0% { - width: ${(props) => props.theme.dimensions!.width}; - height: ${(props) => props.theme.dimensions!.height}; - border-radius: 12px; - } - 100% { - width: 200px; - height: 100px; - } + to { + width: 200px; + height: 100px; + } `; -const fadeIn = keyframes` +const reducedIn = keyframes` from { - opacity: 0; - width: ${(props) => props.theme.dimensions!.width}; - height: ${(props) => props.theme.dimensions!.height}; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - width: ${(props) => props.theme.dimensions!.width}; - height: ${(props) => props.theme.dimensions!.height}; - } + opacity: 0; + } + to { + opacity: 1; + } `; - -const fadeOut = keyframes` +const reducedOut = keyframes` from { - opacity: 1; - width: ${(props) => props.theme.dimensions!.width}; - height: ${(props) => props.theme.dimensions!.height}; - } - to { - opacity: 0; - transform: scale(0.9); - width: ${(props) => props.theme.dimensions!.width}; - height: ${(props) => props.theme.dimensions!.height}; - } + opacity: 1; + } + to { + opacity: 0; + } +`; +const panelIn = keyframes` + from { + opacity: 0; + transform: scale(0.94) translateY(8px); + } + to { + opacity: 1; + transform: none; + } +`; +const panelOut = keyframes` + from { + opacity: 1; + transform: none; + } + to { + opacity: 0; + transform: scale(0.94) translateY(8px); + } `; const scaleAnimation = keyframes` from { @@ -310,60 +345,83 @@ const WidgetContainer = styled.div<{ $modal?: boolean }>` right: ${(props) => (props.$modal ? '50%' : '10px')}; bottom: ${(props) => (props.$modal ? '50%' : '10px')}; z-index: 1001; - transform-origin: 100% 100%; display: block; &.modal { transform: translate(50%, 50%); } - &.open { - animation: css ${createBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - } - &.close { - animation: css ${closeBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - } align-items: center; text-align: left; + + @media only screen and (max-width: 768px) { + right: 0; + bottom: 0; + &.modal { + transform: none; + } + } `; const StyledContainer = styled.div<{ $isOpen: boolean }>` all: initial; + box-sizing: border-box; max-height: ${(props) => props.theme.dimensions!.maxHeight}; max-width: ${(props) => props.theme.dimensions!.maxWidth}; width: ${(props) => props.theme.dimensions!.width}; height: ${(props) => props.theme.dimensions!.height}; position: relative; flex-direction: column; - justify-content: space-between; bottom: 0; left: 0; background-color: ${(props) => props.theme.primary.bg}; - font-family: sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; - border-radius: 12px; + padding: 0; + overflow: hidden; + border-radius: ${radii.panel}; box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.05), - 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 26px 26px 0px 26px; + 0 12px 44px rgba(0, 0, 0, 0.18), + 0 2px 8px rgba(0, 0, 0, 0.1); + transform-origin: ${(props) => + props.theme.dimensions!.size === 'large' ? 'center' : '100% 100%'}; animation: ${({ $isOpen, theme }) => theme.dimensions!.size === 'large' ? $isOpen ? css` - ${fadeIn} 150ms ease-in forwards + ${panelIn} 200ms cubic-bezier(0.16, 1, 0.3, 1) forwards ` : css` - ${fadeOut} 150ms ease-in forwards + ${panelOut} 180ms cubic-bezier(0.4, 0, 1, 1) forwards ` : $isOpen ? css` - ${openContainer} 150ms ease-in forwards + ${openContainer} 150ms ease-in ` : css` ${closeContainer} 250ms ease-in forwards `}; + transition: + width 280ms cubic-bezier(0.4, 0, 0.2, 1), + height 280ms cubic-bezier(0.4, 0, 0.2, 1); + + @media (prefers-reduced-motion: reduce) { + animation: ${({ $isOpen }) => + $isOpen + ? css` + ${reducedIn} 120ms ease-out forwards + ` + : css` + ${reducedOut} 120ms ease-in forwards + `}; + transition: none; + } + @media only screen and (max-width: 768px) { - max-height: 100vh; - max-width: 80vw; - overflow: auto; + width: 100vw; + height: 100dvh; + max-width: 100vw; + max-height: 100dvh; + border-radius: 0; } `; @@ -381,13 +439,20 @@ const FloatingButton = styled.div<{ align-items: center; bottom: 16px; color: white; - font-family: sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; right: 16px; font-weight: 500; - border-radius: 9999px; + border-radius: ${radii.full}; background: ${(props) => props.$bgcolor}; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.18), + 0 2px 6px rgba(0, 0, 0, 0.12); cursor: pointer; + transition: + transform 0.2s ease-in-out, + box-shadow 0.2s ease-in-out; animation: ${(props) => props.$isAnimatingButton ? css` @@ -395,89 +460,28 @@ const FloatingButton = styled.div<{ ` : 'none'}; &:hover { - transform: scale(1.1); - transition: transform 0.2s ease-in-out; + transform: translateY(-2px); + box-shadow: + 0 12px 30px rgba(0, 0, 0, 0.22), + 0 3px 8px rgba(0, 0, 0, 0.14); } - &:not(:hover) { - transition: transform 0.2s ease-in-out; + @media (prefers-reduced-motion: reduce) { + transition: none; + animation: none; } `; -const CancelButton = styled.button` - cursor: pointer; - position: absolute; - top: 0; - right: 0; - margin: 8px; - width: 30px; - padding: 0; - background-color: transparent; - border: none; - outline: none; - color: inherit; - transition: opacity 0.3s ease; - opacity: 0.6; - &:hover { - opacity: 1; - } - .white-filter { - filter: invert(100%); - } -`; - -const Header = styled.div` - display: flex; - align-items: flex-start; -`; - -const ContentWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 2px; - margin-left: 8px; -`; - -const Title = styled.h3` - font-size: 14px; - font-weight: normal; - color: ${(props) => props.theme.primary.text}; - margin: 0; -`; - -const Description = styled.p` - font-size: 13.75px; - color: ${(props) => props.theme.secondary.text}; - margin: 0; - padding: 0; -`; - -const Conversation = styled.div` - height: 100%; - border-radius: 6px; - text-align: left; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: ${(props) => props.theme.secondary.bg} transparent; /* thumb color track color */ -`; -const ActionsRow = styled.div` - display: flex; - align-items: center; - gap: 2px; - clear: both; - padding: 0 2px 2px 2px; -`; -const ActionButton = styled.button` +const IconButton = styled.button` display: inline-flex; align-items: center; justify-content: center; - gap: 4px; - height: 24px; - padding: 0 6px; + flex-shrink: 0; + width: 30px; + height: 30px; + padding: 0; border: none; - border-radius: 6px; + border-radius: ${radii.sm}; background-color: transparent; color: ${(props) => props.theme.secondary.text}; - font-size: 11px; - font-family: sans-serif; cursor: pointer; transition: background-color 0.15s ease, @@ -489,66 +493,267 @@ const ActionButton = styled.button` } &:focus-visible { - outline: 2px solid #8860db; + outline: 2px solid ${(props) => props.theme.accent!.base}; outline-offset: 1px; } `; + +const ExpandButton = styled(IconButton)` + @media only screen and (max-width: 768px) { + display: none; + } +`; + +const Header = styled.div` + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + box-sizing: border-box; + padding: 12px 12px 12px 16px; + border-bottom: 1px solid ${(props) => props.theme.hairline}; +`; + +const Avatar = styled.img` + width: 32px; + height: 32px; + flex-shrink: 0; + border-radius: ${radii.full}; + object-fit: cover; +`; + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + flex: 1; +`; + +const HeaderActions = styled.div` + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +`; + +const Title = styled.h3` + font-size: 14px; + font-weight: 600; + line-height: 1.3; + color: ${(props) => props.theme.primary.text}; + margin: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const Description = styled.p` + font-size: 12.5px; + line-height: 1.35; + color: ${(props) => props.theme.secondary.text}; + margin: 0; + padding: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const Conversation = styled.div` + height: 100%; + box-sizing: border-box; + padding: 16px; + text-align: left; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 20px; + scrollbar-width: thin; + scrollbar-color: ${(props) => props.theme.secondary.bg} transparent; /* thumb color track color */ + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-thumb { + background-color: ${(props) => props.theme.secondary.bg}; + border-radius: ${radii.full}; + } + &::-webkit-scrollbar-track { + background: transparent; + } +`; +const ActionsRow = styled.div<{ $pinned?: boolean }>` + display: flex; + align-items: center; + gap: 2px; + padding: 0; + opacity: ${(props) => (props.$pinned ? 1 : 0)}; + transition: opacity 0.15s ease; + + @media (hover: none) { + opacity: 1; + } +`; +const reactPop = keyframes` + 0% { + transform: scale(1); + } + 45% { + transform: scale(1.3); + } + 100% { + transform: scale(1); + } +`; +const ActionButton = styled.button<{ + $active?: boolean; + $tone?: 'accent' | 'danger'; +}>` + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 26px; + padding: 0 7px; + border: none; + border-radius: ${radii.sm}; + background-color: transparent; + color: ${(props) => props.theme.secondary.text}; + font-size: 11px; + font-family: inherit; + cursor: pointer; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover { + background-color: ${(props) => props.theme.secondary.bg}; + color: ${(props) => props.theme.primary.text}; + } + + &:active { + transform: scale(0.92); + } + + &:focus-visible { + outline: 2px solid ${(props) => props.theme.accent!.base}; + outline-offset: 1px; + } + + ${(props) => + props.$active && + css` + color: ${props.$tone === 'danger' + ? props.theme.danger!.text + : props.theme.accent!.base}; + background-color: ${props.$tone === 'danger' + ? props.theme.danger!.soft + : props.theme.accent!.soft}; + + &:hover { + color: ${props.$tone === 'danger' + ? props.theme.danger!.text + : props.theme.accent!.base}; + background-color: ${props.$tone === 'danger' + ? props.theme.danger!.soft + : props.theme.accent!.soft}; + } + + svg { + animation: ${reactPop} 280ms cubic-bezier(0.34, 1.56, 0.64, 1); + } + + @media (prefers-reduced-motion: reduce) { + svg { + animation: none; + } + } + `} +`; +const Turn = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +`; +const ActionHint = styled.span` + margin-left: 4px; + font-size: 11px; + line-height: 1; + color: ${(props) => props.theme.danger!.text}; + animation: ${settleIn} 0.18s ease-out; + + @media (prefers-reduced-motion: reduce) { + animation: none; + } +`; const MessageBubble = styled.div<{ $type: MESSAGE_TYPE }>` - display: block; - font-size: 16px; - position: relative; - width: 100%; - float: right; - margin: 0px; + display: flex; + flex-direction: column; + align-items: ${(props) => + props.$type === 'QUESTION' ? 'flex-end' : 'flex-start'}; + gap: 6px; + min-width: 0; + font-size: 15px; animation: ${settleIn} 0.22s ease-out; @media (prefers-reduced-motion: reduce) { animation: none; } - /* Contains the floated Message so the bubble keeps its height. */ - &::after { - content: ''; - display: table; - clear: both; + + &:hover .dgpt-actions, + &:focus-within .dgpt-actions { + opacity: 1; } `; const Message = styled.div<{ $type: MESSAGE_TYPE }>` - background: ${(props) => - props.$type === 'QUESTION' - ? 'linear-gradient(to bottom right, #8860DB, #6D42C5)' - : props.theme.secondary.bg}; - color: ${(props) => - props.$type === 'ANSWER' ? props.theme.primary.text : '#fff'}; - border: none; - float: ${(props) => (props.$type === 'QUESTION' ? 'right' : 'left')}; - max-width: ${(props) => (props.$type === 'ANSWER' ? '90%' : '80%')}; - overflow: auto; - margin: 4px; display: block; - line-height: 1.5; - padding: 12px; - border-radius: 6px; + min-width: 0; + line-height: 1.6; overflow-wrap: break-word; + ${(props) => + props.$type === 'QUESTION' + ? css` + max-width: 85%; + padding: 10px 16px; + border-radius: ${radii.lg}; + border-bottom-right-radius: ${radii.sm}; + background: linear-gradient( + to bottom right, + ${props.theme.accent!.base}, + ${props.theme.accent!.strong} + ); + color: ${props.theme.accent!.contrast}; + ` + : css` + width: 100%; + padding: 0; + background: transparent; + color: ${props.theme.primary.text}; + `} `; const Markdown = styled.div` a { - color: #9971ec; + color: ${(props) => props.theme.accent!.link}; text-decoration: underline; text-underline-offset: 2px; } a:hover { - color: #8860db; + color: ${(props) => props.theme.accent!.base}; } pre { - padding: 8px; - width: 90%; + box-sizing: border-box; + padding: 12px; + width: 100%; + margin: 12px 0; font-size: 12px; - border-radius: 6px; + line-height: 1.5; + border-radius: ${radii.md}; overflow-x: auto; - background-color: #1b1c1f; - color: #fff; + background-color: ${(props) => props.theme.secondary.bg}; + border: 1px solid ${(props) => props.theme.hairline}; + color: ${(props) => props.theme.primary.text}; } h1 { @@ -569,17 +774,16 @@ const Markdown = styled.div` code:not(pre code) { border-radius: 6px; - padding: 1px 3px; - font-size: 12px; - display: inline-block; - background-color: #646464; - color: #fff; + padding: 1.5px 5px; + font-size: 0.875em; + background-color: ${(props) => props.theme.secondary.bg}; + border: 1px solid ${(props) => props.theme.hairline}; + color: ${(props) => props.theme.primary.text}; } code { white-space: pre-wrap; overflow-wrap: break-word; - word-break: break-all; } ul { @@ -604,15 +808,15 @@ const Markdown = styled.div` line-height: 1.625; } .dgpt-table-container { - margin: 20px 0; + margin: 16px 0; width: 100%; - overflow-x: scroll !important; - border: 1px solid #a2a2ab; - border-radius: 6px; + overflow-x: auto; + border: 1px solid ${(props) => props.theme.hairline}; + border-radius: ${radii.md}; -webkit-overflow-scrolling: touch; -ms-overflow-style: scrollbar; scrollbar-width: thin; - scrollbar-color: #a2a2ab #38383b; + scrollbar-color: ${(props) => props.theme.secondary.bg} transparent; } table, @@ -633,7 +837,7 @@ const Markdown = styled.div` td, .dgpt-td { padding: 10px; - border-bottom: 1px solid #a2a2ab; + border-bottom: 1px solid ${(props) => props.theme.hairline}; font-size: 14px; } th { @@ -644,16 +848,34 @@ const Markdown = styled.div` } `; const ErrorAlert = styled.div` - color: #b91c1c; - border: 0.1px solid #b91c1c; display: flex; - padding: 4px; - margin: 11.2px; - opacity: 90%; - max-width: 70%; + align-items: flex-start; + gap: 10px; + box-sizing: border-box; + width: 100%; + padding: 12px 14px; font-weight: 400; - border-radius: 6px; - justify-content: space-evenly; + color: ${(props) => props.theme.danger!.text}; + background-color: ${(props) => props.theme.danger!.soft}; + border: 1px solid ${(props) => props.theme.danger!.border}; + border-radius: ${radii.md}; +`; +const ErrorBody = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 0; +`; +const ErrorTitle = styled.h5` + margin: 0 0 2px 0; + font-size: 13px; + font-weight: 600; +`; +const ErrorText = styled.span` + font-size: 12.5px; + line-height: 1.5; + opacity: 0.9; + overflow-wrap: break-word; `; const shimmerSweep = keyframes` to { @@ -673,9 +895,7 @@ const StatusLine = styled.div` align-items: center; gap: 8px; min-width: 0; - /* Message floats; without this the row wraps beside it. */ - clear: both; - margin: 4px; + max-width: 100%; font-size: 12px; font-family: sans-serif; color: ${(props) => props.theme.secondary.text}; @@ -717,10 +937,10 @@ const ShimmerText = styled.span` `; // Reasoning trace from `thought` events. const Thought = styled.div` - clear: both; - margin: 4px; - padding-left: 8px; - border-left: 2px solid ${(props) => props.theme.secondary.bg}; + width: 100%; + box-sizing: border-box; + padding-left: 10px; + border-left: 2px solid ${(props) => props.theme.hairline}; font-size: 12px; font-family: sans-serif; font-style: italic; @@ -732,19 +952,24 @@ const RetryButton = styled.button` display: inline-flex; align-items: center; gap: 6px; - margin-top: 8px; - padding: 6px 10px; - border: 1px solid #b91c1c33; - border-radius: 8px; + margin-top: 10px; + padding: 6px 12px; + border: 1px solid ${(props) => props.theme.danger!.border}; + border-radius: ${radii.full}; background-color: transparent; - color: #b91c1c; + color: ${(props) => props.theme.danger!.text}; font-size: 12px; - font-family: sans-serif; + font-family: inherit; cursor: pointer; transition: background-color 0.15s ease; - &:hover { - background-color: #b91c1c14; + &:hover:not(:disabled) { + background-color: ${(props) => props.theme.danger!.soft}; + } + + &:disabled { + opacity: 0.5; + cursor: default; } `; // Shown only while scrolled away from the latest turn. @@ -762,9 +987,9 @@ const ScrollToLatest = styled.button` align-items: center; justify-content: center; border: none; - border-radius: 9999px; - background-color: #8860db; - color: #fff; + border-radius: ${radii.full}; + background-color: ${(props) => props.theme.accent!.base}; + color: ${(props) => props.theme.accent!.contrast}; line-height: 0; cursor: pointer; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22); @@ -776,18 +1001,32 @@ const ConversationArea = styled.div` flex: 1; min-height: 0; `; +const Composer = styled.div` + flex-shrink: 0; + box-sizing: border-box; + padding: 12px 16px 0 16px; + border-top: 1px solid ${(props) => props.theme.hairline}; +`; const PromptContainer = styled.form` - /* Sole source of the gap above the composer. */ - margin: 16px 12px 0 12px; - padding: 5px 5px 5px 4px; + box-sizing: border-box; + padding: 4px 4px 4px 6px; background-color: ${(props) => props.theme.secondary.bg}; - border: none; - border-radius: 14px; + border: 1px solid ${(props) => props.theme.hairline}; + border-radius: 24px; min-height: ${(props) => props.theme.dimensions!.size == 'large' ? '40px' : '23px'}; max-height: 150px; display: flex; align-items: end; + gap: 6px; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; + + &:focus-within { + border-color: ${(props) => props.theme.accent!.base}; + box-shadow: 0 0 0 3px ${(props) => props.theme.accent!.soft}; + } `; const StyledTextarea = styled.textarea` box-sizing: border-box; @@ -795,12 +1034,11 @@ const StyledTextarea = styled.textarea` border: none; padding: ${(props) => props.theme.dimensions!.size === 'large' - ? '18px 12px 14px 12px' - : '8px 12px 4px 12px'}; + ? '18px 6px 14px 10px' + : '9px 6px 5px 10px'}; background-color: transparent; - font-size: 16px; - font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 15px; + font-family: inherit; border-radius: 6px; color: ${(props) => props.theme.text}; outline: none; @@ -815,125 +1053,153 @@ const StyledTextarea = styled.textarea` max-height: 140px; overflow-y: auto; scrollbar-width: thin; - scrollbar-color: #38383b transparent; + scrollbar-color: ${(props) => props.theme.hairline} transparent; &::-webkit-scrollbar { width: 6px; height: 6px; } &::-webkit-scrollbar-thumb { - background-color: #38383b; - border-radius: 6px; + background-color: ${(props) => props.theme.hairline}; + border-radius: ${radii.full}; } &::-webkit-scrollbar-track { background: transparent; } &::placeholder { text-align: left; + color: ${(props) => props.theme.secondary.text}; } `; const StyledButton = styled.button` display: flex; justify-content: center; align-items: center; - background-image: linear-gradient(to bottom right, #5af0ec, #e80d9d); - background-color: rgba(0, 0, 0, 0.3); - border-radius: 6px; + flex-shrink: 0; + background-color: ${(props) => props.theme.accent!.base}; + color: ${(props) => props.theme.accent!.contrast}; + border-radius: ${radii.full}; min-width: ${(props) => - props.theme.dimensions!.size === 'large' ? '60px' : '40px'}; + props.theme.dimensions!.size === 'large' ? '44px' : '36px'}; + width: ${(props) => + props.theme.dimensions!.size === 'large' ? '44px' : '36px'}; height: ${(props) => - props.theme.dimensions!.size === 'large' ? '60px' : '40px'}; - margin-left: 8px; + props.theme.dimensions!.size === 'large' ? '44px' : '36px'}; + margin: 0; padding: 0px; - border: none; cursor: pointer; outline: none; - &:hover { - opacity: 90%; + transition: + background-color 0.15s ease, + opacity 0.15s ease; + + &:hover:not(:disabled) { + background-color: ${(props) => props.theme.accent!.hover}; } + + &:focus-visible { + outline: 2px solid ${(props) => props.theme.accent!.base}; + outline-offset: 2px; + } + &:disabled { - background-image: linear-gradient(to bottom right, #2d938f, #b31877); + opacity: 0.4; + cursor: default; } `; const HeroContainer = styled.div` - position: relative; - width: 90%; - max-width: 500px; - background-image: linear-gradient(to bottom right, #5af0ec, #ff1bf4); - border-radius: 10px; - margin: 16px auto; - padding: 2px; + box-sizing: border-box; + width: 100%; + max-width: 460px; + margin: auto; + padding: 4px; `; const HeroWrapper = styled.div` display: flex; flex-direction: column; - justify-content: flex-start; - gap: 8px; - align-items: middle; - background-color: ${(props) => props.theme.primary.bg}; - border-radius: 10px; + align-items: flex-start; + gap: 10px; + box-sizing: border-box; + background-color: ${(props) => props.theme.secondary.bg}; + border: 1px solid ${(props) => props.theme.hairline}; + border-radius: ${radii.md}; font-weight: normal; - padding: 12px; + padding: 16px; +`; +const HeroBadge = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: ${radii.sm}; + color: ${(props) => props.theme.accent!.base}; + background-color: ${(props) => props.theme.accent!.soft}; `; const HeroTitle = styled.h3` - color: ${(props) => props.theme.text}; - font-size: 16px; + color: ${(props) => props.theme.primary.text}; + font-size: 15px; + font-weight: 600; margin: 0px; padding: 0px; `; const HeroDescription = styled.p` - color: ${(props) => props.theme.text}; - font-size: 12px; - line-height: 1.5; + color: ${(props) => props.theme.secondary.text}; + font-size: 12.5px; + line-height: 1.55; margin: 0px; padding: 0px; `; const Hyperlink = styled.a` - color: #9971ec; + color: ${(props) => props.theme.accent!.link}; text-decoration: none; + &:hover { + text-decoration: underline; + } `; const Tagline = styled.div` text-align: center; display: block; color: ${(props) => props.theme.secondary.text}; - padding: 12px; - font-size: 12px; + padding: 7px 12px 9px 12px; + font-size: 11px; `; const SourcesList = styled.div` display: flex; - margin: 12px 0px; + width: 100%; + margin: 0; flex-wrap: wrap; - gap: 8px; + align-items: center; + gap: 6px; `; -const SourceLink = styled.a` - color: ${(props) => props.theme.primary.text}; - text-decoration: none; +const SourceChip = styled.span` + color: ${(props) => props.theme.secondary.text}; background: ${(props) => props.theme.secondary.bg}; - padding: 4px 12px; - border-radius: 85px; - font-size: 14px; - transition: opacity 0.2s ease; - display: inline-block; - text-align: center; - max-width: 25%; + border: 1px solid ${(props) => props.theme.hairline}; + padding: 3px 10px; + border-radius: ${radii.full}; + font-size: 12px; + display: inline-flex; + align-items: center; + max-width: min(100%, 220px); + line-height: 1.6; +`; +const SourceLabel = styled.span` + min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - line-height: 1.5; - - &:hover { - opacity: 0.8; - } `; const ExtraButton = styled.button` - color: #9971ec; + color: ${(props) => props.theme.accent!.link}; background: transparent; - border-radius: 85px; - padding: 4px 12px; - font-size: 14px; + border-radius: ${radii.full}; + padding: 3px 8px; + font-size: 12px; + font-family: inherit; border: none; cursor: pointer; transition: opacity 0.2s ease; @@ -955,15 +1221,9 @@ const SourcesComponent = ({ return ( {visibleSources.map((source, idx) => ( - - {source.title} - + + {source.title} + ))} {sources.length > 3 && ( setShowAll(!showAll)}> @@ -977,20 +1237,16 @@ const SourcesComponent = ({ const Hero = ({ title, description, - theme, }: { title: string; description: string; - theme: string; }) => { return ( - + + + {title} {description} @@ -1001,7 +1257,7 @@ export const DocsGPTWidget = (props: WidgetProps) => { const { buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', buttonText = 'Ask a question', - buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', + buttonBg = 'linear-gradient(to bottom right, #8860DB, #6D42C5)', defaultOpen = false, ...coreProps } = props; @@ -1009,7 +1265,7 @@ export const DocsGPTWidget = (props: WidgetProps) => { const [open, setOpen] = React.useState(defaultOpen); const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); const [isFloatingButtonVisible, setIsFloatingButtonVisible] = - React.useState(true); + React.useState(!defaultOpen); React.useEffect(() => { if (isFloatingButtonVisible) @@ -1052,7 +1308,7 @@ export const WidgetCore = ({ heroTitle = 'Welcome to DocsGPT !', heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.', size = 'medium', - theme = 'dark', + theme = 'light', collectFeedback = true, isOpen = false, showSources = true, @@ -1068,10 +1324,13 @@ export const WidgetCore = ({ ); // Auto-follow the stream only while already near the bottom. const [isPinnedToLatest, setIsPinnedToLatest] = React.useState(true); + const [isExpanded, setIsExpanded] = React.useState(false); const [copiedIndex, setCopiedIndex] = React.useState(null); + const [feedbackErrorIndex, setFeedbackErrorIndex] = React.useState< + number | null + >(null); const abortRef = useRef(null); - const isBubbleHovered = useRef(false); const conversationRef = useRef(null); const endMessageRef = React.useRef(null); const promptRef = React.useRef(null); @@ -1131,8 +1390,19 @@ export const WidgetCore = ({ else scrollToLatest(); }, [queries.length, queries[queries.length - 1]?.response, status]); + const setFeedbackAt = (index: number, value?: FEEDBACK) => + setQueries((prev: Query[]) => + prev.map((q, i) => { + if (i !== index) return q; + const updated = { ...q }; + if (value) updated.feedback = value; + else delete updated.feedback; + return updated; + }), + ); + async function handleFeedback(feedback: FEEDBACK, index: number) { - let query = queries[index]; + const query = queries[index]; if (!query.response || !conversationId) { console.log( 'Cannot submit feedback: missing response or conversation ID', @@ -1140,59 +1410,34 @@ export const WidgetCore = ({ return; } - // If clicking the same feedback button that's already active, remove the feedback by sending null - if (query.feedback === feedback) { - try { - const response = await sendFeedback( - { - question: query.prompt, - answer: query.response, - feedback: null, - apikey: apiKey, - conversation_id: conversationId, - question_index: index, - }, - apiHost, - ); + const previous = query.feedback; + const next = previous === feedback ? undefined : feedback; - if (response.status === 200) { - const updatedQuery = { ...query }; - delete updatedQuery.feedback; - setQueries((prev: Query[]) => - prev.map((q, i) => (i === index ? updatedQuery : q)), - ); - } - } catch (err) { - console.error('Failed to submit feedback:', err); - } - return; - } + setFeedbackAt(index, next); try { const response = await sendFeedback( { question: query.prompt, answer: query.response, - feedback: feedback, + feedback: next ?? null, apikey: apiKey, conversation_id: conversationId, question_index: index, }, apiHost, ); - - if (response.status === 200) { - setQueries((prev: Query[]) => { - return prev.map((q, i) => { - if (i === index) { - return { ...q, feedback: feedback }; - } - return q; - }); - }); + if (response.status !== 200) { + throw new Error(`Feedback rejected with status ${response.status}`); } } catch (err) { - console.error('Failed to submit feedback:', err); + console.warn('Feedback not saved:', err); + setFeedbackAt(index, previous); + setFeedbackErrorIndex(index); + setTimeout( + () => setFeedbackErrorIndex((cur) => (cur === index ? null : cur)), + 2600, + ); } } @@ -1331,7 +1576,7 @@ export const WidgetCore = ({ 1600, ); } catch (err) { - console.error('Copy failed:', err); + console.warn('Copy failed:', err); } }; @@ -1422,10 +1667,15 @@ export const WidgetCore = ({ ); }; - const dimensions = + const baseDimensions = typeof size === 'object' && 'custom' in size ? sizesConfig.getCustom(size.custom) : sizesConfig[size]; + const canExpand = size !== 'large'; + const dimensions = + canExpand && isExpanded + ? expandedDimensions(baseDimensions) + : baseDimensions; if (!mounted) return null; return ( @@ -1437,31 +1687,37 @@ export const WidgetCore = ({ $modal={size === 'large'} > -
- - - -
- docs-gpt - - {title} - {description} - -
-
+
+ + + {title} + {description} + + + {canExpand && ( + setIsExpanded((prev) => !prev)} + aria-label={isExpanded ? 'Collapse chat' : 'Expand chat'} + aria-expanded={isExpanded} + title={isExpanded ? 'Collapse' : 'Expand'} + > + {isExpanded ? ( + + ) : ( + + )} + + )} + + + + +
0 ? ( queries?.map((query, index) => { return ( - + {query.prompt && ( )} {query.response ? ( - { - isBubbleHovered.current = true; - }} - $type="ANSWER" - > + {showSources && query.sources && query.sources.length > 0 && @@ -1532,7 +1783,13 @@ export const WidgetCore = ({ {renderStatusLine(query, index)} - + { @@ -1562,15 +1819,12 @@ export const WidgetCore = ({ }} aria-label="Good response" aria-pressed={query.feedback === 'LIKE'} + title="Good response" + $active={query.feedback === 'LIKE'} + $tone="accent" > )} + {feedbackErrorIndex === index && ( + + Couldn't save + + )} ) : ( @@ -1601,15 +1857,13 @@ export const WidgetCore = ({ {query.error ? ( -
-
Network Error
- - {query.error} - + + Network Error + {query.error} handleRetry(index)} @@ -1618,7 +1872,7 @@ export const WidgetCore = ({ Try again -
+
) : ( @@ -1630,15 +1884,11 @@ export const WidgetCore = ({ )} )} -
+ ); }) ) : ( - + )}
{!isPinnedToLatest && queries.length > 0 && ( @@ -1651,7 +1901,7 @@ export const WidgetCore = ({ )}
-
+ - + ) : ( - + )} @@ -1688,7 +1938,7 @@ export const WidgetCore = ({ DocsGPT -
+ } diff --git a/extensions/react-widget/src/requests/streamingApi.ts b/extensions/react-widget/src/requests/streamingApi.ts index b3c39105..9f8c1389 100644 --- a/extensions/react-widget/src/requests/streamingApi.ts +++ b/extensions/react-widget/src/requests/streamingApi.ts @@ -58,7 +58,7 @@ export function fetchAnswerStreaming({ const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); - + let buffer = ''; const emit = (rawLine: string) => { @@ -106,7 +106,7 @@ export function fetchAnswerStreaming({ resolve(); return; } - console.error('Connection failed:', error); + console.warn('Connection failed:', error); reject(error); }); }); diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index b0e7e0db..cfdb2c45 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -19,6 +19,20 @@ declare module 'styled-components' { base: string; highlight: string; }; + accent?: { + base: string; + hover: string; + strong: string; + contrast: string; + soft: string; + link: string; + }; + hairline?: string; + danger?: { + text: string; + soft: string; + border: string; + }; /** Present only in DocsGPTWidget theme (always provided when these styled components render) */ dimensions?: { size: string;