mirror of
https://github.com/unslothai/unsloth.git
synced 2026-05-17 03:56:07 +00:00
Merge branch 'main' into ci/workflow-permissions-and-fewer-skips
This commit is contained in:
commit
0823562525
4 changed files with 124 additions and 8 deletions
1
studio/frontend/package-lock.json
generated
1
studio/frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<HTMLFormElement>) => {
|
||||
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}
|
||||
/>
|
||||
<ComposerAction
|
||||
disabled={disabled || isComposing}
|
||||
blockSend={() => isComposingRef.current}
|
||||
/>
|
||||
<ComposerAction disabled={disabled} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
@ -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<HTMLTextAreaElement>) => {
|
||||
setCompositionState(false);
|
||||
setComposerText(e.currentTarget.value);
|
||||
},
|
||||
[setComposerText, setCompositionState],
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCompositionState(isNativeComposing(e.nativeEvent));
|
||||
setComposerText(e.target.value);
|
||||
},
|
||||
[setComposerText, setCompositionState],
|
||||
);
|
||||
|
||||
return {
|
||||
inputProps: {
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
onChange,
|
||||
},
|
||||
isComposing,
|
||||
isComposingRef: composingRef,
|
||||
};
|
||||
}
|
||||
|
||||
const ComposerAudioUpload: FC = () => {
|
||||
const audioInputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="aui-composer-action-wrapper composer-action-wrapper">
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -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 = () => {
|
|||
<ComposerPrimitive.Input
|
||||
className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm font-[450] outline-none"
|
||||
autoFocus={true}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
|
||||
<ComposerPrimitive.Cancel asChild={true}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Button type="button" variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</ComposerPrimitive.Cancel>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
if (isComposingRef.current) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const newText = aui.composer().getState().text;
|
||||
const originalText = aui.message().getCopyText();
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { toast } from "sonner";
|
|||
import { loadModel, validateModel } from "./api/chat-api";
|
||||
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
|
||||
import {
|
||||
type CompositionEvent,
|
||||
type KeyboardEvent,
|
||||
type MutableRefObject,
|
||||
type ReactElement,
|
||||
|
|
@ -52,6 +53,10 @@ export interface CompareHandle {
|
|||
const IMAGE_ACCEPT = "image/jpeg,image/png,image/webp,image/gif";
|
||||
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
function isNativeComposing(event: Event) {
|
||||
return "isComposing" in event && (event as InputEvent).isComposing === true;
|
||||
}
|
||||
|
||||
function fileToBase64DataURL(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
|
@ -238,7 +243,9 @@ export function SharedComposer({
|
|||
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
|
||||
const [pendingAudio, setPendingAudio] = useState<{ name: string; base64: string } | null>(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const composingRef = useRef(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const audioInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
|
@ -323,7 +330,13 @@ export function SharedComposer({
|
|||
setPendingImages((prev) => prev.filter((p) => p.id !== id));
|
||||
}, []);
|
||||
|
||||
function setCompositionState(next: boolean) {
|
||||
composingRef.current = next;
|
||||
setIsComposing(next);
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (composingRef.current) return;
|
||||
const msg = text.trim();
|
||||
if (!msg && pendingImages.length === 0 && !pendingAudio) return;
|
||||
|
||||
|
|
@ -482,6 +495,9 @@ export function SharedComposer({
|
|||
const busy = running || comparing;
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
// IME composition (Japanese/Chinese/Korean): Enter commits the candidate.
|
||||
// Don't hijack it. See issue #5318.
|
||||
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (!busy) {
|
||||
|
|
@ -490,7 +506,7 @@ export function SharedComposer({
|
|||
}
|
||||
}
|
||||
|
||||
const canSend = (text.trim().length > 0 || pendingImages.length > 0 || pendingAudio !== null) && !busy;
|
||||
const canSend = (text.trim().length > 0 || pendingImages.length > 0 || pendingAudio !== null) && !busy && !isComposing;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -538,7 +554,23 @@ export function SharedComposer({
|
|||
<textarea
|
||||
ref={textareaRef}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={(e) => {
|
||||
// ALWAYS mirror the DOM value into React state, even during IME
|
||||
// composition. The controlled `value` prop must match the DOM at
|
||||
// all times, otherwise any unrelated parent re-render reconciles
|
||||
// the textarea back to the stored value mid-composition — wiping
|
||||
// the IME preedit AND prior committed text (e.g. Tab cycling
|
||||
// candidates erases earlier words). Issue #5318.
|
||||
setCompositionState(isNativeComposing(e.nativeEvent));
|
||||
setText(e.target.value);
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
setCompositionState(true);
|
||||
}}
|
||||
onCompositionEnd={(e: CompositionEvent<HTMLTextAreaElement>) => {
|
||||
setCompositionState(false);
|
||||
setText(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Send to both models..."
|
||||
className="composer-input"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue