diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index dc7d9f9dd3..c7d36992aa 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -506,6 +506,7 @@ async def edit_image( async def ask_user( questions: list[dict], allow_other: bool = True, + timeout_ms: int = 120_000, __event_call__: callable = None, ) -> str: """ @@ -514,6 +515,7 @@ async def ask_user( :param questions: 1-3 question objects, each with id, header, question, and 2-3 options. Each option needs label and description. :param allow_other: Whether users may enter a free-form answer instead of choosing one of the options + :param timeout_ms: How long the browser should keep the prompt open before cancelling it :return: JSON with status and answers keyed by question id """ try: @@ -568,6 +570,9 @@ async def ask_user( } ) + if isinstance(timeout_ms, bool) or not isinstance(timeout_ms, int) or not 60_000 <= timeout_ms <= 240_000: + timeout_ms = 120_000 + if __event_call__ is None: return JSONCodec.dumps( { @@ -583,6 +588,7 @@ async def ask_user( 'data': { 'questions': normalized_questions, 'allow_other': allow_other, + 'timeout_ms': timeout_ms, }, } ) diff --git a/src/lib/components/chat/AskUserCard.svelte b/src/lib/components/chat/AskUserCard.svelte index 4c69789c04..d4ba1f8fc9 100644 --- a/src/lib/components/chat/AskUserCard.svelte +++ b/src/lib/components/chat/AskUserCard.svelte @@ -1,5 +1,6 @@ {#if show} - + - - {$i18n.t('Planning question')} + + {$i18n.t('Assistant needs your input')} - + {$i18n.t('Question')} {questionIndex + 1} {$i18n.t('of')} - {questions.length} ยท - {$i18n.t('Paused while visible')} + {questions.length} @@ -177,12 +203,12 @@ isSelectedOption(question, optionIndex) && advance()} > {#if selectedAnswer?.type === 'other'} updateOther(question, (event.currentTarget as HTMLInputElement).value)} + on:keydown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + advance(); + } + }} /> {/if} {/if} @@ -264,18 +296,27 @@ {/if} - (questionIndex -= 1)} - > - {$i18n.t('Previous')} - + + + {$i18n.t('Cancel')} + + (questionIndex -= 1)} + > + {$i18n.t('Previous')} + + {#if questionIndex < questions.length - 1} (questionIndex += 1)} > {$i18n.t('Next')} @@ -283,7 +324,7 @@ {:else} submit()} > diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 9f168455dc..a6ece6f15c 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -166,6 +166,7 @@ let showAskUserDialog = false; let askUserQuestions: any[] = []; let askUserAllowOther = true; + let askUserTimeoutMs: number | null = null; let selectedModels = ['']; let atSelectedModel: Model | undefined; @@ -1151,6 +1152,8 @@ eventCallback = cb; askUserQuestions = data?.questions ?? []; askUserAllowOther = data?.allow_other ?? true; + askUserTimeoutMs = + typeof data?.timeout_ms === 'number' && data.timeout_ms > 0 ? data.timeout_ms : null; showAskUserDialog = true; } else if (type.startsWith('terminal:')) { terminalEventHandler(type, data); @@ -4177,9 +4180,14 @@ show: showAskUserDialog, questions: askUserQuestions, allowOther: askUserAllowOther, + timeoutMs: askUserTimeoutMs, onConfirm: (value) => { showAskUserDialog = false; eventCallback(value); + }, + onCancel: () => { + showAskUserDialog = false; + eventCallback({ status: 'cancelled', answers: {} }); } }} onQueueSendNow={sendQueuedMessageNow} @@ -4275,9 +4283,14 @@ show: showAskUserDialog, questions: askUserQuestions, allowOther: askUserAllowOther, + timeoutMs: askUserTimeoutMs, onConfirm: (value) => { showAskUserDialog = false; eventCallback(value); + }, + onCancel: () => { + showAskUserDialog = false; + eventCallback({ status: 'cancelled', answers: {} }); } }} onQueueSendNow={sendQueuedMessageNow} diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 6b40cec2b9..f4959a928c 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -113,6 +113,15 @@ const i18n = getContext('i18n'); + type AskUserPrompt = { + show: boolean; + questions: any[]; + allowOther: boolean; + timeoutMs: number | null; + onConfirm: (value: any) => void; + onCancel: () => void; + }; + export let onUpload: Function = (e) => {}; export let onChange: Function = () => {}; export let onWebSearchToggle: Function = () => {}; @@ -171,11 +180,13 @@ export let onQueueEdit: (id: string) => void = () => {}; export let onQueueDelete: (id: string) => void = () => {}; export let onUpdate: (data?: { file?: any }) => void = () => {}; - export let askUser = { + export let askUser: AskUserPrompt = { show: false, questions: [], allowOther: true, - onConfirm: (_value: any) => {} + timeoutMs: null, + onConfirm: (_value: any) => {}, + onCancel: () => {} }; export let chatTasks = []; @@ -1593,9 +1604,13 @@ show={askUser.show} questions={askUser.questions} allowOther={askUser.allowOther} + timeoutMs={askUser.timeoutMs} on:confirm={(e) => { askUser.onConfirm(e.detail); }} + on:cancel={() => { + askUser.onCancel(); + }} /> {/if}