diff --git a/extensions/react-widget/src/components/ComposerControls.tsx b/extensions/react-widget/src/components/ComposerControls.tsx index fb92fa30..22ff535b 100644 --- a/extensions/react-widget/src/components/ComposerControls.tsx +++ b/extensions/react-widget/src/components/ComposerControls.tsx @@ -525,6 +525,9 @@ export const VoiceWaveform = ({ const context = canvas?.getContext('2d'); if (!canvas || !context) return; + // Safari exposed webkitSpeechRecognition from 14.1 but roundRect only + // from 16.4, so dictation can run where the rounded path throws. + const canRoundRect = typeof context.roundRect === 'function'; const levels = new Array(WAVEFORM_BARS).fill(0); // Bare `Uint8Array` widens to ArrayBufferLike, which the analyser // signature rejects. @@ -573,9 +576,13 @@ export const VoiceWaveform = ({ const x = index * slot + (slot - barWidth) / 2; const y = (height - barHeight) / 2; context.globalAlpha = 0.35 + 0.65 * (index / WAVEFORM_BARS); - context.beginPath(); - context.roundRect(x, y, barWidth, barHeight, radius); - context.fill(); + if (canRoundRect) { + context.beginPath(); + context.roundRect(x, y, barWidth, barHeight, radius); + context.fill(); + } else { + context.fillRect(x, y, barWidth, barHeight); + } } context.globalAlpha = 1; }; diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index a6a1bc97..7af67307 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1208,7 +1208,7 @@ const SourcesList = styled.div` gap: 6px; `; -const SourceChip = styled.span` +const SourceChip = styled.a` color: ${(props) => props.theme.secondary.text}; background: ${(props) => props.theme.secondary.bg}; border: 1px solid ${(props) => props.theme.hairline}; @@ -1219,6 +1219,21 @@ const SourceChip = styled.span` align-items: center; max-width: min(100%, 220px); line-height: 1.6; + text-decoration: none; + cursor: pointer; + transition: + border-color 0.15s ease, + color 0.15s ease; + + &:hover { + color: ${(props) => props.theme.primary.text}; + border-color: ${(props) => props.theme.accent!.base}; + } + + &:focus-visible { + outline: 2px solid ${(props) => props.theme.accent!.base}; + outline-offset: 2px; + } `; const SourceLabel = styled.span` min-width: 0; @@ -1255,7 +1270,13 @@ const SourcesComponent = ({ return ( {visibleSources.map((source, idx) => ( - + {source.title} ))} @@ -1739,7 +1760,10 @@ export const WidgetCore = ({ const isDictating = recordingState === 'recording' || recordingState === 'transcribing'; const canSubmit = - prompt.trim().length > 0 && !sendBlockedReason && !isDictating; + prompt.trim().length > 0 && + !sendBlockedReason && + !isDictating && + status !== 'loading'; const submitPrompt = async () => { if (!canSubmit) return; diff --git a/extensions/react-widget/src/requests/attachmentsApi.ts b/extensions/react-widget/src/requests/attachmentsApi.ts index d9762bf9..13117d11 100644 --- a/extensions/react-widget/src/requests/attachmentsApi.ts +++ b/extensions/react-widget/src/requests/attachmentsApi.ts @@ -5,6 +5,8 @@ * response carries. Auth is the agent key as `api_key` in the multipart body. */ +const UPLOAD_TIMEOUT_MS = 2 * 60 * 1000; + interface UploadAttachmentOptions { file: File; apiKey: string; @@ -91,7 +93,13 @@ export function uploadAttachment({ reject(new DOMException('Upload aborted', 'AbortError')); }; + xhr.ontimeout = () => { + signal?.removeEventListener('abort', onAbort); + reject(new Error('Upload timed out.')); + }; + xhr.open('POST', `${apiHost}/api/store_attachment`); + xhr.timeout = UPLOAD_TIMEOUT_MS; xhr.send(formData); }); }