From 92f4d7204292aee8924715dcc2825437ae60febf Mon Sep 17 00:00:00 2001 From: Sophie Turner Date: Wed, 2 Jul 2025 18:08:17 +0500 Subject: [PATCH 1/3] feat: add simultaneous attempts selector to HiveChat --- .../hiveChat/SimultaneousAttemptsSelector.tsx | 164 ++++++++++++++++++ src/people/hiveChat/index.tsx | 20 ++- src/services/index.ts | 6 +- src/store/chat.ts | 4 +- 4 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 src/people/hiveChat/SimultaneousAttemptsSelector.tsx diff --git a/src/people/hiveChat/SimultaneousAttemptsSelector.tsx b/src/people/hiveChat/SimultaneousAttemptsSelector.tsx new file mode 100644 index 000000000..0e98425ff --- /dev/null +++ b/src/people/hiveChat/SimultaneousAttemptsSelector.tsx @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import MaterialIcon from '@material/react-material-icon'; + +interface SimultaneousAttemptsSelectorProps { + value: number; + onChange: (value: number) => void; + disabled?: boolean; +} + +const Container = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-right: 12px; + position: relative; + background-color: #f7f9fc; + border-radius: 8px; + padding: 2px 8px; + border: 1px solid #e0e4e8; + transition: all 0.2s ease; + + &:hover { + background-color: #eef2f7; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + } + + &:focus-within { + border-color: #4285f4; + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); + } +`; + +const Label = styled.span` + font-size: 12px; + color: #5f6368; + white-space: nowrap; + font-weight: 500; + user-select: none; + display: none; + + @media (min-width: 768px) { + display: block; + } +`; + +const Dropdown = styled.select` + padding: 6px 10px; + border: 2px solid transparent; + border-radius: 6px; + background: white; + font-size: 14px; + font-weight: 500; + color: #3c4043; + cursor: pointer; + transition: all 0.2s ease; + min-width: 45px; + appearance: none; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + + &:hover { + background-color: #f5f8ff; + } + + &:focus { + outline: none; + border-color: #4285f4; + box-shadow: 0 1px 3px rgba(66, 133, 244, 0.3); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: #f1f3f4; + color: #9aa0a6; + } +`; + +const Icon = styled(MaterialIcon)` + font-size: 18px; + color: #4285f4; + margin-right: -2px; +`; + +const DropdownWrapper = styled.div` + position: relative; + display: flex; + align-items: center; +`; + +const ArrowIcon = styled(MaterialIcon)` + font-size: 16px; + color: #5f6368; + position: absolute; + right: 6px; + pointer-events: none; +`; + +const Tooltip = styled.div<{ visible: boolean }>` + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%) translateY(${(props) => (props.visible ? '0' : '5px')}); + background-color: rgba(32, 33, 36, 0.9); + color: white; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + visibility: ${(props) => (props.visible ? 'visible' : 'hidden')}; + opacity: ${(props) => (props.visible ? 1 : 0)}; + transition: all 0.2s ease; + z-index: 100; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + max-width: 250px; + text-align: center; + + &:after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: rgba(32, 33, 36, 0.9) transparent transparent transparent; + } +`; + +const SimultaneousAttemptsSelector: React.FC = ({ + value, + onChange, + disabled = false +}) => { + const [showTooltip, setShowTooltip] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + onChange(Number(e.target.value)); + }; + + return ( + setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)}> + Number of simultaneous attempts + + + + + + + + + + + + ); +}; + +export default SimultaneousAttemptsSelector; diff --git a/src/people/hiveChat/index.tsx b/src/people/hiveChat/index.tsx index 24a738948..8a7ec62f3 100644 --- a/src/people/hiveChat/index.tsx +++ b/src/people/hiveChat/index.tsx @@ -19,6 +19,7 @@ import VisualScreenViewer from '../widgetViews/workspace/VisualScreenViewer.tsx' import { ModelOption } from './modelSelector.tsx'; import { ActionArtifactRenderer } from './ActionArtifactRenderer'; import ChatStatusDisplay from './ChatStatusDisplay.tsx'; +import SimultaneousAttemptsSelector from './SimultaneousAttemptsSelector'; import SplashScreen from './ChatSplashScreen'; @@ -308,6 +309,13 @@ const AttachIcon = styled(MaterialIcon)` margin-right: 2px; `; +const SimultaneousAttemptsContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-right: 12px; +`; + const TabContainer = styled.div` display: flex; margin-left: 10px; @@ -593,6 +601,7 @@ export const HiveChatView: React.FC = observer(() => { label: 'Open AI - 4o', value: 'gpt-4o' }); + const [simultaneousAttempts, setSimultaneousAttempts] = useState(1); const [artifactTab, setArtifactTab] = useState<'visual' | 'code' | 'text' | 'logs'>('logs'); const [updatedTabs, setUpdatedTabs] = useState>({ logs: false, @@ -794,7 +803,8 @@ export const HiveChatView: React.FC = observer(() => { 'Build', undefined, pdfUrl, - actionArtifact + actionArtifact, + simultaneousAttempts ); if (sentMessage === undefined) { @@ -1206,7 +1216,8 @@ export const HiveChatView: React.FC = observer(() => { 'Build', undefined, pdfUrl, - actionArtifact + actionArtifact, + simultaneousAttempts ); if (sentMessage) { @@ -1603,6 +1614,11 @@ export const HiveChatView: React.FC = observer(() => { placeholder="Type your message..." disabled={isSending} /> + {isPdfUploadEnabled && ( setIsUploadModalOpen(true)} disabled={isSending}> Attach diff --git a/src/services/index.ts b/src/services/index.ts index 5ae864dfa..f604eb4c7 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -220,7 +220,8 @@ export class ChatService { contextTags?: ContextTag[], pdfUrl?: string, modelSelection?: string, - actionArtifact?: Artifact + actionArtifact?: Artifact, + simultaneousAttempts?: number ): Promise { try { if (!uiStore.meInfo) return undefined; @@ -235,7 +236,8 @@ export class ChatService { workspaceUUID, pdf_url: pdfUrl, modelSelection, - mode + mode, + simultaneous_attempts: simultaneousAttempts || 1 }; if ( diff --git a/src/store/chat.ts b/src/store/chat.ts index 96807b42d..8c9404f6a 100644 --- a/src/store/chat.ts +++ b/src/store/chat.ts @@ -360,6 +360,7 @@ export class ChatHistoryStore implements ChatStore { contextTags?: ContextTag[], pdfUrl?: string, actionArtifact?: Artifact, + simultaneousAttempts?: number, artifacts?: Artifact[] ): Promise { try { @@ -372,7 +373,8 @@ export class ChatHistoryStore implements ChatStore { contextTags, pdfUrl, modelSelection, - actionArtifact + actionArtifact, + simultaneousAttempts ); if (newMessage) { if (!this.chatMessages[chat_id]) { From 80ce2f05e68719f93b9db73a280ea4b403403bc3 Mon Sep 17 00:00:00 2001 From: Sophie Turner Date: Wed, 2 Jul 2025 19:42:18 +0500 Subject: [PATCH 2/3] improvement: enhance hive chat UX with simultaneous attempts selector --- .../hiveChat/SimultaneousAttemptsSelector.tsx | 261 +++++++++++------- 1 file changed, 161 insertions(+), 100 deletions(-) diff --git a/src/people/hiveChat/SimultaneousAttemptsSelector.tsx b/src/people/hiveChat/SimultaneousAttemptsSelector.tsx index 0e98425ff..e68963224 100644 --- a/src/people/hiveChat/SimultaneousAttemptsSelector.tsx +++ b/src/people/hiveChat/SimultaneousAttemptsSelector.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import styled from 'styled-components'; import MaterialIcon from '@material/react-material-icon'; @@ -9,122 +9,123 @@ interface SimultaneousAttemptsSelectorProps { } const Container = styled.div` + position: relative; display: flex; align-items: center; - gap: 8px; margin-right: 12px; - position: relative; - background-color: #f7f9fc; - border-radius: 8px; - padding: 2px 8px; - border: 1px solid #e0e4e8; - transition: all 0.2s ease; - - &:hover { - background-color: #eef2f7; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); - } - - &:focus-within { - border-color: #4285f4; - box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); - } `; -const Label = styled.span` - font-size: 12px; +const DropdownButton = styled.button<{ disabled?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + padding: 8px 8px 8px 16px; + background: transparent; + border: 1px solid #5f6368; + border-radius: 8px; color: #5f6368; - white-space: nowrap; - font-weight: 500; - user-select: none; - display: none; - - @media (min-width: 768px) { - display: block; - } -`; - -const Dropdown = styled.select` - padding: 6px 10px; - border: 2px solid transparent; - border-radius: 6px; - background: white; + cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; font-size: 14px; font-weight: 500; - color: #3c4043; - cursor: pointer; - transition: all 0.2s ease; - min-width: 45px; - appearance: none; - text-align: center; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); - - &:hover { - background-color: #f5f8ff; - } + transition: all 0.2s; + height: fit-content; + align-self: center; - &:focus { - outline: none; + &:hover:not(:disabled) { + background: rgba(95, 99, 104, 0.1); border-color: #4285f4; - box-shadow: 0 1px 3px rgba(66, 133, 244, 0.3); } &:disabled { opacity: 0.6; - cursor: not-allowed; - background-color: #f1f3f4; + border-color: #e4e7eb; color: #9aa0a6; } `; const Icon = styled(MaterialIcon)` - font-size: 18px; - color: #4285f4; - margin-right: -2px; + font-size: 16px; + margin-right: 4px; `; -const DropdownWrapper = styled.div` - position: relative; - display: flex; - align-items: center; +const NumberText = styled.span` + font-size: 14px; + margin-right: 4px; `; const ArrowIcon = styled(MaterialIcon)` font-size: 16px; - color: #5f6368; - position: absolute; - right: 6px; - pointer-events: none; `; -const Tooltip = styled.div<{ visible: boolean }>` +const DropdownMenu = styled.div<{ visible: boolean }>` position: absolute; - top: -40px; - left: 50%; - transform: translateX(-50%) translateY(${(props) => (props.visible ? '0' : '5px')}); - background-color: rgba(32, 33, 36, 0.9); - color: white; - padding: 6px 12px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - visibility: ${(props) => (props.visible ? 'visible' : 'hidden')}; + bottom: calc(100% + 4px); + left: 0; + background-color: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 100; opacity: ${(props) => (props.visible ? 1 : 0)}; + visibility: ${(props) => (props.visible ? 'visible' : 'hidden')}; + transform: translateY(${(props) => (props.visible ? '0' : '10px')}); transition: all 0.2s ease; - z-index: 100; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - max-width: 250px; - text-align: center; + width: 100%; + min-width: 120px; +`; + +const MenuItem = styled.div<{ active?: boolean }>` + padding: 10px 16px; + color: #3c4043; + font-size: 14px; + cursor: pointer; + transition: background-color 0.1s ease; + background-color: ${(props) => (props.active ? '#f1f3f6' : 'transparent')}; + display: flex; + align-items: center; + + &:hover { + background-color: #f5f8ff; + } +`; + +const Checkmark = styled(MaterialIcon)` + margin-left: auto; + font-size: 18px; + color: #4285f4; +`; + +const Tooltip = styled.div<{ visible: boolean; top: number; left: number }>` + position: fixed; + left: ${(props) => props.left}px; + top: ${(props) => props.top}px; + background: rgba(31, 41, 55, 0.95); + color: #ffffff; + padding: 8px 16px; + border-radius: 6px; + font-size: 15px; + font-weight: 500; + max-width: 200px; + opacity: ${(props) => (props.visible ? 1 : 0)}; + visibility: ${(props) => (props.visible ? 'visible' : 'hidden')}; + transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + pointer-events: none; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + z-index: 1000; + transform: translateX(-50%) translateY(-100%) translateY(-8px); - &:after { + &::after { content: ''; position: absolute; - top: 100%; left: 50%; - margin-left: -5px; - border-width: 5px; + bottom: -8px; + transform: translateX(-50%); + border-width: 8px 8px 0 8px; border-style: solid; - border-color: rgba(32, 33, 36, 0.9) transparent transparent transparent; + border-color: rgba(31, 41, 55, 0.95) transparent transparent transparent; } `; @@ -133,30 +134,90 @@ const SimultaneousAttemptsSelector: React.FC onChange, disabled = false }) => { + const [showMenu, setShowMenu] = useState(false); const [showTooltip, setShowTooltip] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 }); + const buttonRef = useRef(null); + + const handleSelect = (selectedValue: number) => { + if (!disabled) { + onChange(selectedValue); + setShowMenu(false); + } + }; - const handleChange = (e: React.ChangeEvent) => { - onChange(Number(e.target.value)); + const toggleMenu = () => { + if (!disabled) { + setShowMenu(!showMenu); + } }; + const handleMouseEnter = () => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setTooltipPosition({ + top: rect.top - 8, + left: rect.left + rect.width / 2 + }); + setShowTooltip(true); + } + }; + + const handleMouseLeave = () => { + setShowTooltip(false); + }; + + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + showMenu && + !event + .composedPath() + .some((el) => el instanceof Element && el.classList.contains('dropdown-area')) + ) { + setShowMenu(false); + } + }; + + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, [showMenu]); + return ( - setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)}> - Number of simultaneous attempts - - - - - - - - + + + + {value} - + + + {showTooltip && ( + + Attempts + + )} + + + handleSelect(1)}> + 1 version + {value === 1 && } + + handleSelect(2)}> + 2 versions + {value === 2 && } + + handleSelect(3)}> + 3 versions + {value === 3 && } + + ); }; From 9284f60adc07918a76016392262e06e9a1ccb95e Mon Sep 17 00:00:00 2001 From: Sophie Turner Date: Tue, 26 Aug 2025 01:09:39 -0700 Subject: [PATCH 3/3] fix unit test --- src/store/__test__/chatStore.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/store/__test__/chatStore.spec.ts b/src/store/__test__/chatStore.spec.ts index 3b523a1f4..016cf3e8e 100644 --- a/src/store/__test__/chatStore.spec.ts +++ b/src/store/__test__/chatStore.spec.ts @@ -136,6 +136,7 @@ describe('ChatHistoryStore', () => { undefined, undefined, 'gpt-4o', + undefined, undefined ); }); @@ -161,6 +162,7 @@ describe('ChatHistoryStore', () => { undefined, undefined, 'claude-3-5-sonnet-latest', + undefined, undefined ); }); @@ -186,6 +188,7 @@ describe('ChatHistoryStore', () => { undefined, undefined, 'o3-mini', + undefined, undefined ); });