diff --git a/studio/frontend/package-lock.json b/studio/frontend/package-lock.json index ed2ceb550..21d31d81e 100644 --- a/studio/frontend/package-lock.json +++ b/studio/frontend/package-lock.json @@ -12,6 +12,7 @@ "@assistant-ui/react": "0.12.28", "@assistant-ui/react-markdown": "0.12.11", "@assistant-ui/react-streamdown": "0.1.11", + "@assistant-ui/tap": "0.5.10", "@base-ui/react": "^1.2.0", "@dagrejs/dagre": "^2.0.4", "@dagrejs/graphlib": "^3.0.4", diff --git a/studio/frontend/package.json b/studio/frontend/package.json index 22088c68a..463fbd926 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -20,6 +20,7 @@ "@assistant-ui/react": "0.12.28", "@assistant-ui/react-markdown": "0.12.11", "@assistant-ui/react-streamdown": "0.1.11", + "@assistant-ui/tap": "0.5.10", "@base-ui/react": "^1.2.0", "@dagrejs/dagre": "^2.0.4", "@dagrejs/graphlib": "^3.0.4", diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 31a1fb21e..dc3d1c21b 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -51,6 +51,7 @@ import { useAuiEvent, useAuiState, } from "@assistant-ui/react"; +import { flushResourcesSync } from "@assistant-ui/tap"; import { ArrowDownIcon, ArrowUpIcon, @@ -72,6 +73,8 @@ import { import { Copy01Icon, Delete02Icon, Edit03Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { + type ChangeEvent, + type CompositionEvent, type FC, type FormEvent, useCallback, @@ -282,13 +285,15 @@ const PendingAudioChip: FC = () => { }; const Composer: FC<{ disabled?: boolean }> = ({ disabled }) => { + const { inputProps, isComposing, isComposingRef } = useImeComposerInputHandlers(); + const handleSubmit = useCallback( (event: FormEvent) => { - if (disabled) { + if (disabled || isComposingRef.current) { event.preventDefault(); } }, - [disabled], + [disabled, isComposingRef], ); const composerContent = ( @@ -304,8 +309,12 @@ const Composer: FC<{ disabled?: boolean }> = ({ disabled }) => { autoFocus={!disabled} disabled={disabled} aria-label="Message input" + {...inputProps} + /> + isComposingRef.current} /> - ); @@ -330,6 +339,64 @@ const Composer: FC<{ disabled?: boolean }> = ({ disabled }) => { ); }; +function isNativeComposing(event: Event) { + return "isComposing" in event && (event as InputEvent).isComposing === true; +} + +function useImeComposerInputHandlers() { + const aui = useAui(); + const composingRef = useRef(false); + const [isComposing, setIsComposing] = useState(false); + + const setCompositionState = useCallback((next: boolean) => { + composingRef.current = next; + setIsComposing(next); + }, []); + + const setComposerText = useCallback( + (value: string) => { + const composer = aui.composer(); + if (!composer.getState().isEditing) { + return; + } + flushResourcesSync(() => { + composer.setText(value); + }); + }, + [aui], + ); + + const onCompositionStart = useCallback(() => { + setCompositionState(true); + }, [setCompositionState]); + + const onCompositionEnd = useCallback( + (e: CompositionEvent) => { + setCompositionState(false); + setComposerText(e.currentTarget.value); + }, + [setComposerText, setCompositionState], + ); + + const onChange = useCallback( + (e: ChangeEvent) => { + setCompositionState(isNativeComposing(e.nativeEvent)); + setComposerText(e.target.value); + }, + [setComposerText, setCompositionState], + ); + + return { + inputProps: { + onCompositionStart, + onCompositionEnd, + onChange, + }, + isComposing, + isComposingRef: composingRef, + }; +} + const ComposerAudioUpload: FC = () => { const audioInputRef = useRef(null); const setPendingAudio = useChatRuntimeStore((s) => s.setPendingAudio); @@ -607,7 +674,10 @@ const ToolStatusDisplay: FC = () => { ); }; -const ComposerAction: FC<{ disabled?: boolean }> = ({ disabled }) => { +const ComposerAction: FC<{ disabled?: boolean; blockSend?: () => boolean }> = ({ + disabled, + blockSend, +}) => { return (
@@ -650,6 +720,11 @@ const ComposerAction: FC<{ disabled?: boolean }> = ({ disabled }) => { variant="default" size="icon" disabled={disabled} + onClick={(event) => { + if (blockSend?.()) { + event.preventDefault(); + } + }} className="aui-composer-send size-8 rounded-full" aria-label="Send message" > @@ -903,6 +978,7 @@ const UserActionBar: FC = () => { const EditComposer: FC = () => { const aui = useAui(); + const { inputProps, isComposingRef } = useImeComposerInputHandlers(); const resendAfterCancelRef = useRef(false); useAuiEvent("thread.runEnd", () => { @@ -919,16 +995,22 @@ const EditComposer: FC = () => {
-