diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 170cccaaca..f52e88fbb5 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -45,6 +45,13 @@ type ChatInputAreaProps = { theme?: Theme | null isResponding?: boolean disabled?: boolean + /** + * Controls whether pressing Enter sends the message. + * - true (default): Enter sends, Shift+Enter inserts newline + * - false: Enter inserts newline, Shift+Enter sends + * Useful for CJK (Japanese/Korean/Chinese) IME users who expect Enter to insert newlines. + */ + sendOnEnter?: boolean } const ChatInputArea = ({ readonly, @@ -61,6 +68,7 @@ const ChatInputArea = ({ theme, isResponding, disabled, + sendOnEnter = true, }: ChatInputAreaProps) => { const { t } = useTranslation() const { notify } = useToastContext() @@ -131,7 +139,14 @@ const ChatInputArea = ({ }, 50) } const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { + // Determine if this key combo should trigger send: + // sendOnEnter=true (default): Enter sends, Shift+Enter inserts newline + // sendOnEnter=false: Shift+Enter sends, Enter inserts newline + const isSendCombo = sendOnEnter + ? (e.key === 'Enter' && !e.shiftKey) + : (e.key === 'Enter' && e.shiftKey) + + if (isSendCombo && !e.nativeEvent.isComposing) { // if isComposing, exit if (isComposingRef.current) return diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index c3a02c798d..0011e32c72 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -75,6 +75,7 @@ export type ChatProps = { inputDisabled?: boolean sidebarCollapseState?: boolean hideAvatar?: boolean + sendOnEnter?: boolean onHumanInputFormSubmit?: (formToken: string, formData: any) => Promise getHumanInputNodeData?: (nodeID: string) => any } @@ -119,6 +120,7 @@ const Chat: FC = ({ inputDisabled, sidebarCollapseState, hideAvatar, + sendOnEnter, onHumanInputFormSubmit, getHumanInputNodeData, }) => { @@ -363,6 +365,7 @@ const Chat: FC = ({ theme={themeBuilder?.theme} isResponding={isResponding} readonly={readonly} + sendOnEnter={sendOnEnter} /> ) } diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index b781eae918..2c6abbfcab 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -58,6 +58,15 @@ const ChatWrapper = () => { appSourceType, } = useEmbeddedChatbotContext() + // Read sendOnEnter from URL params (e.g., ?sendOnEnter=false) + const sendOnEnter = useMemo(() => { + if (typeof window === 'undefined') + return true + const urlParams = new URLSearchParams(window.location.search) + const param = urlParams.get('sendOnEnter') + return param !== 'false' + }, []) + const appConfig = useMemo(() => { const config = appParams || {} @@ -321,6 +330,7 @@ const ChatWrapper = () => { themeBuilder={themeBuilder} switchSibling={doSwitchSibling} inputDisabled={inputDisabled} + sendOnEnter={sendOnEnter} questionIcon={ initUserVariables?.avatar_url ? ( diff --git a/web/public/embed.js b/web/public/embed.js index 54aa6a95b1..d5eabc0533 100644 --- a/web/public/embed.js +++ b/web/public/embed.js @@ -135,6 +135,11 @@ config.baseUrl || `https://${config.isDev ? "dev." : ""}udify.app`; const targetOrigin = new URL(baseUrl).origin; + // Pass sendOnEnter config as URL parameter + if (config.sendOnEnter === false) { + params.set('sendOnEnter', 'false'); + } + // pre-check the length of the URL const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`; // 1) CREATE the iframe immediately, so it can load in the background: diff --git a/web/public/embed.min.js b/web/public/embed.min.js index 42132e0359..7c366f8f2e 100644 --- a/web/public/embed.min.js +++ b/web/public/embed.min.js @@ -48,7 +48,7 @@ transition-property: width, height; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; - `;async function embedChatbot(){let isDragging=false;if(!config||!config.token){console.error(`${configKey} is empty or token is not provided`);return}async function compressAndEncodeBase64(input){const uint8Array=(new TextEncoder).encode(input);const compressedStream=new Response(new Blob([uint8Array]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();const compressedUint8Array=new Uint8Array(await compressedStream);return btoa(String.fromCharCode(...compressedUint8Array))}async function getCompressedInputsFromConfig(){const inputs=config?.inputs||{};const compressedInputs={};await Promise.all(Object.entries(inputs).map(async([key,value])=>{compressedInputs[key]=await compressAndEncodeBase64(value)}));return compressedInputs}async function getCompressedSystemVariablesFromConfig(){const systemVariables=config?.systemVariables||{};const compressedSystemVariables={};await Promise.all(Object.entries(systemVariables).map(async([key,value])=>{compressedSystemVariables[`sys.${key}`]=await compressAndEncodeBase64(value)}));return compressedSystemVariables}async function getCompressedUserVariablesFromConfig(){const userVariables=config?.userVariables||{};const compressedUserVariables={};await Promise.all(Object.entries(userVariables).map(async([key,value])=>{compressedUserVariables[`user.${key}`]=await compressAndEncodeBase64(value)}));return compressedUserVariables}const params=new URLSearchParams({...await getCompressedInputsFromConfig(),...await getCompressedSystemVariablesFromConfig(),...await getCompressedUserVariablesFromConfig()});const baseUrl=config.baseUrl||`https://${config.isDev?"dev.":""}udify.app`;const targetOrigin=new URL(baseUrl).origin;const iframeUrl=`${baseUrl}/chatbot/${config.token}?${params}`;const preloadedIframe=createIframe();preloadedIframe.style.display="none";document.body.appendChild(preloadedIframe);if(iframeUrl.length>2048){console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load")}function createIframe(){const iframe=document.createElement("iframe");iframe.allow="fullscreen;microphone";iframe.title="dify chatbot bubble window";iframe.id=iframeId;iframe.src=iframeUrl;iframe.style.cssText=originalIframeStyleText;return iframe}function resetIframePosition(){if(window.innerWidth<=640)return;const targetIframe=document.getElementById(iframeId);const targetButton=document.getElementById(buttonId);if(targetIframe&&targetButton){const buttonRect=targetButton.getBoundingClientRect();const viewportCenterY=window.innerHeight/2;const buttonCenterY=buttonRect.top+buttonRect.height/2;if(buttonCenterY{if(event.origin!==targetOrigin)return;const targetIframe=document.getElementById(iframeId);if(!targetIframe||event.source!==targetIframe.contentWindow)return;if(event.data.type==="dify-chatbot-iframe-ready"){targetIframe.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:true,isDraggable:!!config.draggable}},targetOrigin)}if(event.data.type==="dify-chatbot-expand-change"){toggleExpand()}});function createButton(){const containerDiv=document.createElement("div");Object.entries(config.containerProps||{}).forEach(([key,value])=>{if(key==="className"){containerDiv.classList.add(...value.split(" "))}else if(key==="style"){if(typeof value==="object"){Object.assign(containerDiv.style,value)}else{containerDiv.style.cssText=value}}else if(typeof value==="function"){containerDiv.addEventListener(key.replace(/^on/,"").toLowerCase(),value)}else{containerDiv[key]=value}});containerDiv.id=buttonId;const styleSheet=document.createElement("style");document.head.appendChild(styleSheet);styleSheet.sheet.insertRule(` + `;async function embedChatbot(){let isDragging=false;if(!config||!config.token){console.error(`${configKey} is empty or token is not provided`);return}async function compressAndEncodeBase64(input){const uint8Array=(new TextEncoder).encode(input);const compressedStream=new Response(new Blob([uint8Array]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();const compressedUint8Array=new Uint8Array(await compressedStream);return btoa(String.fromCharCode(...compressedUint8Array))}async function getCompressedInputsFromConfig(){const inputs=config?.inputs||{};const compressedInputs={};await Promise.all(Object.entries(inputs).map(async([key,value])=>{compressedInputs[key]=await compressAndEncodeBase64(value)}));return compressedInputs}async function getCompressedSystemVariablesFromConfig(){const systemVariables=config?.systemVariables||{};const compressedSystemVariables={};await Promise.all(Object.entries(systemVariables).map(async([key,value])=>{compressedSystemVariables[`sys.${key}`]=await compressAndEncodeBase64(value)}));return compressedSystemVariables}async function getCompressedUserVariablesFromConfig(){const userVariables=config?.userVariables||{};const compressedUserVariables={};await Promise.all(Object.entries(userVariables).map(async([key,value])=>{compressedUserVariables[`user.${key}`]=await compressAndEncodeBase64(value)}));return compressedUserVariables}const params=new URLSearchParams({...await getCompressedInputsFromConfig(),...await getCompressedSystemVariablesFromConfig(),...await getCompressedUserVariablesFromConfig()});const baseUrl=config.baseUrl||`https://${config.isDev?"dev.":""}udify.app`;const targetOrigin=new URL(baseUrl).origin;if(config.sendOnEnter===false){params.set("sendOnEnter","false")}const iframeUrl=`${baseUrl}/chatbot/${config.token}?${params}`;const preloadedIframe=createIframe();preloadedIframe.style.display="none";document.body.appendChild(preloadedIframe);if(iframeUrl.length>2048){console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load")}function createIframe(){const iframe=document.createElement("iframe");iframe.allow="fullscreen;microphone";iframe.title="dify chatbot bubble window";iframe.id=iframeId;iframe.src=iframeUrl;iframe.style.cssText=originalIframeStyleText;return iframe}function resetIframePosition(){if(window.innerWidth<=640)return;const targetIframe=document.getElementById(iframeId);const targetButton=document.getElementById(buttonId);if(targetIframe&&targetButton){const buttonRect=targetButton.getBoundingClientRect();const viewportCenterY=window.innerHeight/2;const buttonCenterY=buttonRect.top+buttonRect.height/2;if(buttonCenterY{if(event.origin!==targetOrigin)return;const targetIframe=document.getElementById(iframeId);if(!targetIframe||event.source!==targetIframe.contentWindow)return;if(event.data.type==="dify-chatbot-iframe-ready"){targetIframe.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:true,isDraggable:!!config.draggable}},targetOrigin)}if(event.data.type==="dify-chatbot-expand-change"){toggleExpand()}});function createButton(){const containerDiv=document.createElement("div");Object.entries(config.containerProps||{}).forEach(([key,value])=>{if(key==="className"){containerDiv.classList.add(...value.split(" "))}else if(key==="style"){if(typeof value==="object"){Object.assign(containerDiv.style,value)}else{containerDiv.style.cssText=value}}else if(typeof value==="function"){containerDiv.addEventListener(key.replace(/^on/,"").toLowerCase(),value)}else{containerDiv[key]=value}});containerDiv.id=buttonId;const styleSheet=document.createElement("style");document.head.appendChild(styleSheet);styleSheet.sheet.insertRule(` #${containerDiv.id} { position: fixed; bottom: var(--${containerDiv.id}-bottom, 1rem);