Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/people/hiveChat/SimultaneousAttemptsSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import React, { useState, useRef } 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`
position: relative;
display: flex;
align-items: center;
margin-right: 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;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
height: fit-content;
align-self: center;

&:hover:not(:disabled) {
background: rgba(95, 99, 104, 0.1);
border-color: #4285f4;
}

&:disabled {
opacity: 0.6;
border-color: #e4e7eb;
color: #9aa0a6;
}
`;

const Icon = styled(MaterialIcon)`
font-size: 16px;
margin-right: 4px;
`;

const NumberText = styled.span`
font-size: 14px;
margin-right: 4px;
`;

const ArrowIcon = styled(MaterialIcon)`
font-size: 16px;
`;

const DropdownMenu = styled.div<{ visible: boolean }>`
position: absolute;
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;
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 {
content: '';
position: absolute;
left: 50%;
bottom: -8px;
transform: translateX(-50%);
border-width: 8px 8px 0 8px;
border-style: solid;
border-color: rgba(31, 41, 55, 0.95) transparent transparent transparent;
}
`;

const SimultaneousAttemptsSelector: React.FC<SimultaneousAttemptsSelectorProps> = ({
value,
onChange,
disabled = false
}) => {
const [showMenu, setShowMenu] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
const buttonRef = useRef<HTMLButtonElement>(null);

const handleSelect = (selectedValue: number) => {
if (!disabled) {
onChange(selectedValue);
setShowMenu(false);
}
};

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 (
<Container>
<DropdownButton
ref={buttonRef}
onClick={toggleMenu}
disabled={disabled}
className="dropdown-area"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Icon icon="layers" />
<NumberText>{value}</NumberText>
<ArrowIcon icon="arrow_drop_down" />
</DropdownButton>

{showTooltip && (
<Tooltip visible={showTooltip} top={tooltipPosition.top} left={tooltipPosition.left}>
Attempts
</Tooltip>
)}

<DropdownMenu visible={showMenu} className="dropdown-area">
<MenuItem active={value === 1} onClick={() => handleSelect(1)}>
1 version
{value === 1 && <Checkmark icon="check" />}
</MenuItem>
<MenuItem active={value === 2} onClick={() => handleSelect(2)}>
2 versions
{value === 2 && <Checkmark icon="check" />}
</MenuItem>
<MenuItem active={value === 3} onClick={() => handleSelect(3)}>
3 versions
{value === 3 && <Checkmark icon="check" />}
</MenuItem>
</DropdownMenu>
</Container>
);
};

export default SimultaneousAttemptsSelector;
20 changes: 18 additions & 2 deletions src/people/hiveChat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ActionArtifactRenderer } from './ActionArtifactRenderer';
import ChatStatusDisplay from './ChatStatusDisplay.tsx';
import StaktrakRecorder from './StaktrakRecorder';
import axios from 'axios';
import SimultaneousAttemptsSelector from './SimultaneousAttemptsSelector';
import SplashScreen from './ChatSplashScreen';
import { ERROR_MESSAGES } from '../utils/Constants';

Expand Down Expand Up @@ -312,6 +313,13 @@ const AttachButton = styled.button<{ disabled: boolean }>`
}
`;

const SimultaneousAttemptsContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
margin-right: 12px;
`;

const AttachIcon = styled(MaterialIcon)`
font-size: 16px;
margin-right: 2px;
Expand Down Expand Up @@ -595,6 +603,7 @@ export const HiveChatView: React.FC = observer(() => {
const [codeArtifact, setCodeArtifacts] = useState<Artifact[]>();
const [isActionSend, setIsActionSend] = useState(false);
const [pdfUrl, setPdfUrl] = useState('');
const [simultaneousAttempts, setSimultaneousAttempts] = useState<number>(1);
const { isEnabled: isVerboseLoggingEnabled } = useFeatureFlag('verbose_logging_sw');
const { isEnabled: isArtifactLoggingEnabled } = useFeatureFlag('log_artefact');
const { isEnabled: isPdfUploadEnabled } = useFeatureFlag('chat_pdf');
Expand Down Expand Up @@ -928,7 +937,8 @@ export const HiveChatView: React.FC = observer(() => {
'Build',
undefined,
pdfUrl,
actionArtifact
actionArtifact,
simultaneousAttempts
);

if (sentMessage === undefined) {
Expand Down Expand Up @@ -1534,7 +1544,8 @@ export const HiveChatView: React.FC = observer(() => {
'Build',
undefined,
pdfUrl,
actionArtifact
actionArtifact,
simultaneousAttempts
);

if (sentMessage) {
Expand Down Expand Up @@ -2085,6 +2096,11 @@ export const HiveChatView: React.FC = observer(() => {
}
disabled={isSending || !isWorkspaceComplete || isPATExpired}
/>
<SimultaneousAttemptsSelector
value={simultaneousAttempts}
onChange={setSimultaneousAttempts}
disabled={isSending}
/>
{isPdfUploadEnabled && (
<AttachButton
onClick={() => setIsUploadModalOpen(true)}
Expand Down
6 changes: 4 additions & 2 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ export class ChatService {
contextTags?: ContextTag[],
pdfUrl?: string,
modelSelection?: string,
actionArtifact?: Artifact
actionArtifact?: Artifact,
simultaneousAttempts?: number
): Promise<ChatMessage | undefined> {
try {
if (!uiStore.meInfo) return undefined;
Expand All @@ -236,7 +237,8 @@ export class ChatService {
workspaceUUID,
pdf_url: pdfUrl,
modelSelection,
mode
mode,
simultaneous_attempts: simultaneousAttempts || 1
};

if (
Expand Down
3 changes: 3 additions & 0 deletions src/store/__test__/chatStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ describe('ChatHistoryStore', () => {
undefined,
undefined,
'gpt-4o',
undefined,
undefined
);
});
Expand All @@ -161,6 +162,7 @@ describe('ChatHistoryStore', () => {
undefined,
undefined,
'claude-3-5-sonnet-latest',
undefined,
undefined
);
});
Expand All @@ -186,6 +188,7 @@ describe('ChatHistoryStore', () => {
undefined,
undefined,
'o3-mini',
undefined,
undefined
);
});
Expand Down
4 changes: 3 additions & 1 deletion src/store/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export class ChatHistoryStore implements ChatStore {
contextTags?: ContextTag[],
pdfUrl?: string,
actionArtifact?: Artifact,
simultaneousAttempts?: number,
artifacts?: Artifact[]
): Promise<ChatMessage | undefined> {
try {
Expand All @@ -372,7 +373,8 @@ export class ChatHistoryStore implements ChatStore {
contextTags,
pdfUrl,
modelSelection,
actionArtifact
actionArtifact,
simultaneousAttempts
);
if (newMessage) {
if (!this.chatMessages[chat_id]) {
Expand Down
Loading