refac
This commit is contained in:
1 parent
2c01d59335
commit
4465f52a3e
6 files changed
+467
-10
No files matched your search
@@ -498,6 +498,114 @@ async def edit_image(
|
|||||||
return JSONCodec.dumps({'error': str(e)})
|
return JSONCodec.dumps({'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# USER INPUT TOOLS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def ask_user(
|
||||||
|
questions: list[dict],
|
||||||
|
allow_other: bool = True,
|
||||||
|
__event_call__: callable = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Ask the user clarifying questions before continuing.
|
||||||
|
Use this when the next step depends on user intent, preference, or a tradeoff that cannot be inferred safely.
|
||||||
|
|
||||||
|
: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
|
||||||
|
:return: JSON with status and answers keyed by question id
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not isinstance(questions, list) or not 1 <= len(questions) <= 3:
|
||||||
|
raise ValueError('ask_user requires 1-3 questions.')
|
||||||
|
|
||||||
|
normalized_questions = []
|
||||||
|
seen_ids = set()
|
||||||
|
for index, question in enumerate(questions):
|
||||||
|
if not isinstance(question, dict):
|
||||||
|
raise ValueError('Each question must be an object.')
|
||||||
|
|
||||||
|
question_id = str(question.get('id') or '').strip()[:64]
|
||||||
|
if not question_id:
|
||||||
|
raise ValueError('Each question requires a non-empty id.')
|
||||||
|
if question_id in seen_ids:
|
||||||
|
raise ValueError(f'Duplicate question id: {question_id}')
|
||||||
|
seen_ids.add(question_id)
|
||||||
|
|
||||||
|
options = question.get('options')
|
||||||
|
if not isinstance(options, list) or not 2 <= len(options) <= 3:
|
||||||
|
raise ValueError('Each question requires 2-3 options.')
|
||||||
|
|
||||||
|
normalized_options = []
|
||||||
|
for option in options:
|
||||||
|
if not isinstance(option, dict):
|
||||||
|
raise ValueError('Each option must be an object.')
|
||||||
|
|
||||||
|
label = str(option.get('label') or '').strip()[:80]
|
||||||
|
description = str(option.get('description') or '').strip()[:240]
|
||||||
|
if not label or not description:
|
||||||
|
raise ValueError('Each option requires a label and description.')
|
||||||
|
|
||||||
|
normalized_options.append(
|
||||||
|
{
|
||||||
|
'label': label,
|
||||||
|
'description': description,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
question_text = str(question.get('question') or '').strip()[:500]
|
||||||
|
if not question_text:
|
||||||
|
raise ValueError('Each question requires question text.')
|
||||||
|
|
||||||
|
normalized_questions.append(
|
||||||
|
{
|
||||||
|
'id': question_id,
|
||||||
|
'header': str(question.get('header') or '').strip()[:48] or f'Question {index + 1}',
|
||||||
|
'question': question_text,
|
||||||
|
'options': normalized_options,
|
||||||
|
'allow_other': bool(question.get('allow_other', allow_other)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if __event_call__ is None:
|
||||||
|
return JSONCodec.dumps(
|
||||||
|
{
|
||||||
|
'status': 'error',
|
||||||
|
'error': 'User input requires an active browser session with WebSocket connection.',
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
output = await __event_call__(
|
||||||
|
{
|
||||||
|
'type': 'request:user_input',
|
||||||
|
'data': {
|
||||||
|
'questions': normalized_questions,
|
||||||
|
'allow_other': allow_other,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(output, dict):
|
||||||
|
return JSONCodec.dumps({'status': 'error', 'error': 'Invalid user input response.'}, ensure_ascii=False)
|
||||||
|
if output.get('error'):
|
||||||
|
return JSONCodec.dumps({'status': 'error', 'error': output.get('error')}, ensure_ascii=False)
|
||||||
|
if output.get('status') == 'cancelled':
|
||||||
|
return JSONCodec.dumps({'status': 'cancelled', 'answers': {}}, ensure_ascii=False)
|
||||||
|
|
||||||
|
return JSONCodec.dumps(
|
||||||
|
{
|
||||||
|
'status': 'answered',
|
||||||
|
'answers': output.get('answers', {}),
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f'ask_user error: {e}')
|
||||||
|
return JSONCodec.dumps({'status': 'error', 'error': str(e)}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# CODE INTERPRETER TOOLS
|
# CODE INTERPRETER TOOLS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ from open_webui.models.tools import Tools
|
|||||||
from open_webui.models.users import UserModel
|
from open_webui.models.users import UserModel
|
||||||
from open_webui.tools.builtin import (
|
from open_webui.tools.builtin import (
|
||||||
add_memory,
|
add_memory,
|
||||||
|
ask_user,
|
||||||
calculate_timestamp,
|
calculate_timestamp,
|
||||||
create_automation,
|
create_automation,
|
||||||
create_calendar_event,
|
create_calendar_event,
|
||||||
@@ -583,6 +584,9 @@ async def get_builtin_tools(
|
|||||||
if is_builtin_tool_enabled('time'):
|
if is_builtin_tool_enabled('time'):
|
||||||
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
|
builtin_functions.extend([get_current_timestamp, calculate_timestamp])
|
||||||
|
|
||||||
|
if is_builtin_tool_enabled('user_input'):
|
||||||
|
builtin_functions.append(ask_user)
|
||||||
|
|
||||||
metadata = extra_params.get('__metadata__') or {}
|
metadata = extra_params.get('__metadata__') or {}
|
||||||
chat_files = metadata.get('files') or extra_params.get('__files__') or []
|
chat_files = metadata.get('files') or extra_params.get('__files__') or []
|
||||||
has_chat_files = any(
|
has_chat_files = any(
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, getContext } from 'svelte';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { i18n as i18nType } from 'i18next';
|
||||||
|
|
||||||
|
const i18n: Writable<i18nType> = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
type AskUserOption = {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AskUserQuestion = {
|
||||||
|
id: string;
|
||||||
|
header: string;
|
||||||
|
question: string;
|
||||||
|
options: AskUserOption[];
|
||||||
|
allow_other?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DraftAnswer =
|
||||||
|
| {
|
||||||
|
type: 'option';
|
||||||
|
option_index: number;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'other';
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let questions: AskUserQuestion[] = [];
|
||||||
|
export let allowOther = true;
|
||||||
|
|
||||||
|
let answers: Record<string, DraftAnswer> = {};
|
||||||
|
let questionIndex = 0;
|
||||||
|
let wasOpen = false;
|
||||||
|
|
||||||
|
$: if (show && !wasOpen) {
|
||||||
|
answers = {};
|
||||||
|
questionIndex = 0;
|
||||||
|
wasOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (!show && wasOpen) {
|
||||||
|
wasOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionAllowsOther = (question: AskUserQuestion) => question.allow_other ?? allowOther;
|
||||||
|
|
||||||
|
$: question = questions[questionIndex];
|
||||||
|
$: selectedAnswer = question ? answers[question.id] : undefined;
|
||||||
|
|
||||||
|
const hasAnswers = (selected = answers) =>
|
||||||
|
questions.length > 0 &&
|
||||||
|
questions.every((question) => {
|
||||||
|
const answer = selected[question.id];
|
||||||
|
return answer?.type === 'option' || (answer?.type === 'other' && answer.text.trim() !== '');
|
||||||
|
});
|
||||||
|
|
||||||
|
$: complete = hasAnswers();
|
||||||
|
|
||||||
|
const submit = (selected = answers) => {
|
||||||
|
if (!hasAnswers(selected)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized: Record<string, DraftAnswer> = {};
|
||||||
|
for (const question of questions) {
|
||||||
|
const answer = selected[question.id];
|
||||||
|
if (answer?.type === 'option') {
|
||||||
|
normalized[question.id] = answer;
|
||||||
|
} else if (answer?.type === 'other') {
|
||||||
|
normalized[question.id] = {
|
||||||
|
type: 'other',
|
||||||
|
text: answer.text.trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show = false;
|
||||||
|
dispatch('confirm', {
|
||||||
|
status: 'answered',
|
||||||
|
answers: normalized
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const advance = (selected = answers) => {
|
||||||
|
if (questionIndex < questions.length - 1) {
|
||||||
|
questionIndex += 1;
|
||||||
|
} else if (hasAnswers(selected)) {
|
||||||
|
submit(selected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectOption = (question: AskUserQuestion, option: AskUserOption, index: number) => {
|
||||||
|
const selected: Record<string, DraftAnswer> = {
|
||||||
|
...answers,
|
||||||
|
[question.id]: {
|
||||||
|
type: 'option',
|
||||||
|
option_index: index,
|
||||||
|
label: option.label,
|
||||||
|
description: option.description
|
||||||
|
}
|
||||||
|
};
|
||||||
|
answers = selected;
|
||||||
|
advance(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectOther = (question: AskUserQuestion) => {
|
||||||
|
const existing = answers[question.id];
|
||||||
|
answers = {
|
||||||
|
...answers,
|
||||||
|
[question.id]: {
|
||||||
|
type: 'other',
|
||||||
|
text: existing?.type === 'other' ? existing.text : ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOther = (question: AskUserQuestion, text: string) => {
|
||||||
|
answers = {
|
||||||
|
...answers,
|
||||||
|
[question.id]: {
|
||||||
|
type: 'other',
|
||||||
|
text
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedOption = (question: AskUserQuestion, index: number) => {
|
||||||
|
const answer = answers[question.id];
|
||||||
|
return answer?.type === 'option' && answer.option_index === index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectedOther = (question: AskUserQuestion) => answers[question.id]?.type === 'other';
|
||||||
|
|
||||||
|
const otherText = (question: AskUserQuestion) => {
|
||||||
|
const answer = answers[question.id];
|
||||||
|
return answer?.type === 'other' ? answer.text : '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<section class="my-1 rounded-2xl bg-gray-100/70 px-3.5 py-3 dark:bg-white/[0.055]">
|
||||||
|
<div class="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<div class="text-[0.6875rem] font-medium tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
|
{$i18n.t('Planning question')}
|
||||||
|
</div>
|
||||||
|
<div class="text-[0.6875rem] text-gray-500 dark:text-gray-400">
|
||||||
|
{$i18n.t('Question')}
|
||||||
|
{questionIndex + 1}
|
||||||
|
{$i18n.t('of')}
|
||||||
|
{questions.length} ·
|
||||||
|
{$i18n.t('Paused while visible')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
{#if question}
|
||||||
|
{#key question.id}
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium tracking-[-0.01em] text-gray-900 dark:text-gray-100">
|
||||||
|
{question.header}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-xs leading-relaxed text-gray-600 dark:text-gray-300">
|
||||||
|
{question.question}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
{#each question.options || [] as option, optionIndex}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-start gap-2.5 rounded-xl px-2.5 py-1.5 transition-colors {isSelectedOption(
|
||||||
|
question,
|
||||||
|
optionIndex
|
||||||
|
)
|
||||||
|
? 'bg-white shadow-sm dark:bg-white/[0.1]'
|
||||||
|
: 'hover:bg-white/70 dark:hover:bg-white/[0.06]'}"
|
||||||
|
on:click={() => isSelectedOption(question, optionIndex) && advance()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="sr-only"
|
||||||
|
type="radio"
|
||||||
|
name={question.id}
|
||||||
|
value={option.label}
|
||||||
|
checked={isSelectedOption(question, optionIndex)}
|
||||||
|
on:change={() => selectOption(question, option, optionIndex)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="mt-1 flex size-3.5 shrink-0 items-center justify-center rounded-full border {isSelectedOption(
|
||||||
|
question,
|
||||||
|
optionIndex
|
||||||
|
)
|
||||||
|
? 'border-gray-900 dark:border-white'
|
||||||
|
: 'border-gray-300 dark:border-white/25'}"
|
||||||
|
>
|
||||||
|
{#if isSelectedOption(question, optionIndex)}
|
||||||
|
<span class="size-1.5 rounded-full bg-gray-900 dark:bg-white"></span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 text-xs">
|
||||||
|
<span class="text-gray-800 dark:text-gray-100">{option.label}</span>
|
||||||
|
{#if optionIndex === 0}
|
||||||
|
<span class="ml-1.5 text-[0.625rem] text-gray-400 dark:text-gray-500">
|
||||||
|
{$i18n.t('Recommended')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="mt-0.5 block leading-relaxed text-gray-500 dark:text-gray-400">
|
||||||
|
{option.description}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if questionAllowsOther(question)}
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-center gap-2.5 rounded-xl px-2.5 py-1.5 text-xs transition-colors {isSelectedOther(
|
||||||
|
question
|
||||||
|
)
|
||||||
|
? 'bg-white shadow-sm dark:bg-white/[0.1]'
|
||||||
|
: 'hover:bg-white/70 dark:hover:bg-white/[0.06]'}"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="sr-only"
|
||||||
|
type="radio"
|
||||||
|
name={question.id}
|
||||||
|
value="__other__"
|
||||||
|
checked={isSelectedOther(question)}
|
||||||
|
on:change={() => selectOther(question)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="flex size-3.5 shrink-0 items-center justify-center rounded-full border {isSelectedOther(
|
||||||
|
question
|
||||||
|
)
|
||||||
|
? 'border-gray-900 dark:border-white'
|
||||||
|
: 'border-gray-300 dark:border-white/25'}"
|
||||||
|
>
|
||||||
|
{#if selectedAnswer?.type === 'other'}
|
||||||
|
<span class="size-1.5 rounded-full bg-gray-900 dark:bg-white"></span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-200">{$i18n.t('Other')}</span>
|
||||||
|
</label>
|
||||||
|
{#if selectedAnswer?.type === 'other'}
|
||||||
|
<input
|
||||||
|
class="w-full rounded-xl bg-transparent px-2.5 py-1.5 text-xs text-gray-800 outline-none placeholder:text-gray-400 dark:text-gray-100 dark:placeholder:text-gray-500"
|
||||||
|
placeholder={$i18n.t('Type your answer')}
|
||||||
|
value={otherText(question)}
|
||||||
|
on:input={(event) =>
|
||||||
|
updateOther(question, (event.currentTarget as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-2 pt-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg px-2.5 py-1.5 text-xs text-gray-500 transition-colors hover:bg-white/70 hover:text-gray-800 disabled:opacity-30 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-100"
|
||||||
|
disabled={questionIndex === 0}
|
||||||
|
on:click={() => (questionIndex -= 1)}
|
||||||
|
>
|
||||||
|
{$i18n.t('Previous')}
|
||||||
|
</button>
|
||||||
|
{#if questionIndex < questions.length - 1}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-gray-900 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-gray-800 active:scale-[0.98] dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||||
|
on:click={() => (questionIndex += 1)}
|
||||||
|
>
|
||||||
|
{$i18n.t('Next')}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-gray-900 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-gray-800 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||||
|
disabled={!complete}
|
||||||
|
on:click={() => submit()}
|
||||||
|
>
|
||||||
|
{$i18n.t('Submit answers')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
@@ -162,7 +162,10 @@
|
|||||||
let eventConfirmationInputValue = '';
|
let eventConfirmationInputValue = '';
|
||||||
let eventConfirmationInputType = '';
|
let eventConfirmationInputType = '';
|
||||||
let eventConfirmationInputOptions: ({ label?: string; value: string } | string)[] = [];
|
let eventConfirmationInputOptions: ({ label?: string; value: string } | string)[] = [];
|
||||||
let eventCallback = null;
|
let eventCallback: (value: any) => void = () => {};
|
||||||
|
let showAskUserDialog = false;
|
||||||
|
let askUserQuestions: any[] = [];
|
||||||
|
let askUserAllowOther = true;
|
||||||
|
|
||||||
let selectedModels = [''];
|
let selectedModels = [''];
|
||||||
let atSelectedModel: Model | undefined;
|
let atSelectedModel: Model | undefined;
|
||||||
@@ -1144,6 +1147,11 @@
|
|||||||
eventConfirmationInputValue = data?.value ?? '';
|
eventConfirmationInputValue = data?.value ?? '';
|
||||||
eventConfirmationInputType = data?.input?.type ?? data?.type ?? '';
|
eventConfirmationInputType = data?.input?.type ?? data?.type ?? '';
|
||||||
eventConfirmationInputOptions = data?.input?.options ?? data?.options ?? [];
|
eventConfirmationInputOptions = data?.input?.options ?? data?.options ?? [];
|
||||||
|
} else if (type === 'request:user_input') {
|
||||||
|
eventCallback = cb;
|
||||||
|
askUserQuestions = data?.questions ?? [];
|
||||||
|
askUserAllowOther = data?.allow_other ?? true;
|
||||||
|
showAskUserDialog = true;
|
||||||
} else if (type.startsWith('terminal:')) {
|
} else if (type.startsWith('terminal:')) {
|
||||||
terminalEventHandler(type, data);
|
terminalEventHandler(type, data);
|
||||||
} else {
|
} else {
|
||||||
@@ -1600,7 +1608,9 @@
|
|||||||
fileItem.content_type = uploadedFile.meta?.content_type;
|
fileItem.content_type = uploadedFile.meta?.content_type;
|
||||||
fileItem.size = uploadedFile.meta?.size;
|
fileItem.size = uploadedFile.meta?.size;
|
||||||
fileItem.collection_name =
|
fileItem.collection_name =
|
||||||
res.collection_name ?? uploadedFile.meta?.collection_name ?? uploadedFile.collection_name;
|
res.collection_name ??
|
||||||
|
uploadedFile.meta?.collection_name ??
|
||||||
|
uploadedFile.collection_name;
|
||||||
} else {
|
} else {
|
||||||
fileItem.type = 'text';
|
fileItem.type = 'text';
|
||||||
fileItem.file = {
|
fileItem.file = {
|
||||||
@@ -2249,9 +2259,7 @@
|
|||||||
|
|
||||||
chatRequestQueues.update((q) => ({
|
chatRequestQueues.update((q) => ({
|
||||||
...q,
|
...q,
|
||||||
[targetChatId]: (q[targetChatId] ?? []).filter(
|
[targetChatId]: (q[targetChatId] ?? []).filter((m) => !queuedMessageIds.has(m.id))
|
||||||
(m) => !queuedMessageIds.has(m.id)
|
|
||||||
)
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await submitPrompt(combinedPrompt, combinedFiles);
|
await submitPrompt(combinedPrompt, combinedFiles);
|
||||||
@@ -4165,6 +4173,15 @@
|
|||||||
{onUpdate}
|
{onUpdate}
|
||||||
messageQueue={$chatRequestQueues[$chatId] ?? []}
|
messageQueue={$chatRequestQueues[$chatId] ?? []}
|
||||||
{chatTasks}
|
{chatTasks}
|
||||||
|
askUser={{
|
||||||
|
show: showAskUserDialog,
|
||||||
|
questions: askUserQuestions,
|
||||||
|
allowOther: askUserAllowOther,
|
||||||
|
onConfirm: (value) => {
|
||||||
|
showAskUserDialog = false;
|
||||||
|
eventCallback(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onQueueSendNow={sendQueuedMessageNow}
|
onQueueSendNow={sendQueuedMessageNow}
|
||||||
onQueueEdit={editQueuedMessage}
|
onQueueEdit={editQueuedMessage}
|
||||||
onQueueDelete={deleteQueuedMessage}
|
onQueueDelete={deleteQueuedMessage}
|
||||||
@@ -4219,10 +4236,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div id={embedded ? messageInputDropzoneId : undefined} class="pb-2 z-10">
|
||||||
id={embedded ? messageInputDropzoneId : undefined}
|
|
||||||
class="pb-2 z-10"
|
|
||||||
>
|
|
||||||
<MessageInput
|
<MessageInput
|
||||||
bind:this={messageInput}
|
bind:this={messageInput}
|
||||||
{history}
|
{history}
|
||||||
@@ -4257,6 +4271,15 @@
|
|||||||
{onUpdate}
|
{onUpdate}
|
||||||
messageQueue={$chatRequestQueues[$chatId] ?? []}
|
messageQueue={$chatRequestQueues[$chatId] ?? []}
|
||||||
{chatTasks}
|
{chatTasks}
|
||||||
|
askUser={{
|
||||||
|
show: showAskUserDialog,
|
||||||
|
questions: askUserQuestions,
|
||||||
|
allowOther: askUserAllowOther,
|
||||||
|
onConfirm: (value) => {
|
||||||
|
showAskUserDialog = false;
|
||||||
|
eventCallback(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onQueueSendNow={sendQueuedMessageNow}
|
onQueueSendNow={sendQueuedMessageNow}
|
||||||
onQueueEdit={editQueuedMessage}
|
onQueueEdit={editQueuedMessage}
|
||||||
onQueueDelete={deleteQueuedMessage}
|
onQueueDelete={deleteQueuedMessage}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
import Knobs from '../icons/Knobs.svelte';
|
import Knobs from '../icons/Knobs.svelte';
|
||||||
import ValvesModal from '../workspace/common/ValvesModal.svelte';
|
import ValvesModal from '../workspace/common/ValvesModal.svelte';
|
||||||
import Note from '../icons/Note.svelte';
|
import Note from '../icons/Note.svelte';
|
||||||
|
import AskUserCard from './AskUserCard.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import InputModal from '../common/InputModal.svelte';
|
import InputModal from '../common/InputModal.svelte';
|
||||||
import Expand from '../icons/Expand.svelte';
|
import Expand from '../icons/Expand.svelte';
|
||||||
@@ -170,6 +171,12 @@
|
|||||||
export let onQueueEdit: (id: string) => void = () => {};
|
export let onQueueEdit: (id: string) => void = () => {};
|
||||||
export let onQueueDelete: (id: string) => void = () => {};
|
export let onQueueDelete: (id: string) => void = () => {};
|
||||||
export let onUpdate: (data?: { file?: any }) => void = () => {};
|
export let onUpdate: (data?: { file?: any }) => void = () => {};
|
||||||
|
export let askUser = {
|
||||||
|
show: false,
|
||||||
|
questions: [],
|
||||||
|
allowOther: true,
|
||||||
|
onConfirm: (_value: any) => {}
|
||||||
|
};
|
||||||
|
|
||||||
export let chatTasks = [];
|
export let chatTasks = [];
|
||||||
|
|
||||||
@@ -1580,6 +1587,19 @@
|
|||||||
on:click={() => createMessagePair(prompt)}
|
on:click={() => createMessagePair(prompt)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if askUser?.show}
|
||||||
|
<div class="mx-1">
|
||||||
|
<AskUserCard
|
||||||
|
show={askUser.show}
|
||||||
|
questions={askUser.questions}
|
||||||
|
allowOther={askUser.allowOther}
|
||||||
|
on:confirm={(e) => {
|
||||||
|
askUser.onConfirm(e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Task list display -->
|
<!-- Task list display -->
|
||||||
{#if isActive && chatTasks.length > 0}
|
{#if isActive && chatTasks.length > 0}
|
||||||
<div class="mx-1">
|
<div class="mx-1">
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { i18n as i18nType } from 'i18next';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n: Writable<i18nType> = getContext('i18n');
|
||||||
|
|
||||||
const toolLabels = {
|
const toolLabels = {
|
||||||
time: {
|
time: {
|
||||||
label: $i18n.t('Time & Calculation'),
|
label: $i18n.t('Time & Calculation'),
|
||||||
description: $i18n.t('Get current time and perform date/time calculations')
|
description: $i18n.t('Get current time and perform date/time calculations')
|
||||||
},
|
},
|
||||||
|
user_input: {
|
||||||
|
label: $i18n.t('Ask User'),
|
||||||
|
description: $i18n.t('Pause a response to ask the user a clarifying question')
|
||||||
|
},
|
||||||
memory: {
|
memory: {
|
||||||
label: $i18n.t('Memory'),
|
label: $i18n.t('Memory'),
|
||||||
description: $i18n.t('Search and manage user memories')
|
description: $i18n.t('Search and manage user memories')
|
||||||
|
|||||||
Reference in new issue
Block a user