- {attachments.map((item) => (
-
+ ) : null}
+
+ {hasComposerAttachments ? (
+
+ {showRagWarn ? (
+
+ ⚠
+ {tComposer("ragAllDisabled")}
+
+ ) : null}
+
+
+ {attachments.map((item) => (
+
- {(() => {
- const badge = resolveFileProcessingBadge(item, (key, values) => tFileStatus(key, values));
- const FileIcon = resolveFileIcon(item);
- return (
- <>
-
-
-
-
-
- {item.fileName}
-
-
-
- {formatBytes(item.sizeBytes)}
-
-
- {badge.label}
-
- {item.ragOptOut && item.fileCategory !== "image" ? (
+
setPreviewAttachment(item)}
+ aria-label={tComposer("previewAttachment", { name: item.fileName })}
+ >
+ {(() => {
+ const badge = resolveFileProcessingBadge(item, (key, values) => tFileStatus(key, values));
+ const FileIcon = resolveFileIcon(item);
+ return (
+ <>
+
+
+
+
+
+ {item.fileName}
+
+
+
+ {formatBytes(item.sizeBytes)}
+
- {tComposer("ragOff")}
+ {badge.label}
- ) : null}
+ {item.ragOptOut && item.fileCategory !== "image" ? (
+
+ {tComposer("ragOff")}
+
+ ) : null}
+
-
- >
- );
- })()}
-
-
onRemoveAttachment(item.fileID)}
- aria-label={tComposer("removeAttachment", { name: item.fileName })}
+ >
+ );
+ })()}
+
+
onRemoveAttachment(item.fileID)}
+ aria-label={tComposer("removeAttachment", { name: item.fileName })}
+ >
+
+
+
+ ))}
+ {uploadingAttachments.map((item) => (
+
-
-
-
- ))}
- {uploadingAttachments.map((item) => (
-
+ {previewAttachment ? (
+
+ ) : null}
- {previewAttachment ? (
-
- ) : null}
-
- ) : null}
-
-
-
-
handleMentionChange(event.target.value)}
- onPaste={(event) => {
- const files = clipboardFilesFromPaste(event);
- if (files.length === 0) {
- return;
- }
- if (!event.clipboardData.getData("text/plain")) {
- event.preventDefault();
- }
- void onUploadFiles(files);
- }}
- onCompositionStart={() => {
- composingRef.current = true;
- }}
- onCompositionEnd={() => {
- composingRef.current = false;
- }}
- onKeyDown={(event) => {
- if (event.nativeEvent.isComposing || composingRef.current || event.key === "Process" || event.keyCode === 229) {
- return;
- }
- const shouldSend = isSendShortcutEvent(sendShortcut, event);
-
- if (handleMentionKeyDown(event)) {
- return;
- }
-
- if (shouldSend) {
- event.preventDefault();
- if (canSend) {
- void onSendMessage();
- }
- }
- }}
- />
-
-
-
-
{
- setToolsMenuOpen(open);
- if (!open) {
- setToolsMenuHovered(false);
- }
- }}
- >
-
-
+
+ {overlaySelectedSkills ? (
+
+ {selectedSkills.map((skill) => (
+
setToolsMenuHovered(true)}
- onMouseLeave={() => setToolsMenuHovered(false)}
+ onClick={() => onSelectedSkillsChange(selectedSkills.filter((item) => item.id !== skill.id))}
+ aria-label={skill.title}
>
-
+ {skill.trigger || skill.title}
+
-
-
-
- setHoveredTool("upload")}
- onMouseLeave={() => setHoveredTool((prev) => (prev === "upload" ? null : prev))}
- onSelect={(event) => {
- event.preventDefault();
- onSelectUploadTool();
- }}
- >
-
- {tComposer("uploadFile")}
-
- setHoveredTool("screenshot")}
- onMouseLeave={() => setHoveredTool((prev) => (prev === "screenshot" ? null : prev))}
- onSelect={(event) => {
- event.preventDefault();
- onSelectScreenshotTool();
- }}
- >
-
- {tComposer("screenshot")}
-
-
-
-
- {!modelOptionPolicyDisabled ? (
-
- ) : null}
-
- {showMCPToolsButton ? (
-
- ) : null}
+
+ ))}
+
+ ) : null}
+
+ handleMentionChange(event.target.value)}
+ onClick={handleMentionSelectionChange}
+ onKeyUp={handleMentionSelectionChange}
+ onSelect={handleMentionSelectionChange}
+ onPaste={(event) => {
+ const files = clipboardFilesFromPaste(event);
+ if (files.length === 0) {
+ return;
+ }
+ if (!event.clipboardData.getData("text/plain")) {
+ event.preventDefault();
+ }
+ void onUploadFiles(files);
+ }}
+ onCompositionStart={() => {
+ composingRef.current = true;
+ }}
+ onCompositionEnd={() => {
+ composingRef.current = false;
+ }}
+ onKeyDown={(event) => {
+ if (event.nativeEvent.isComposing || composingRef.current || event.key === "Process" || event.keyCode === 229) {
+ return;
+ }
+ const shouldSend = isSendShortcutEvent(sendShortcut, event);
+
+ if (handleMentionKeyDown(event)) {
+ return;
+ }
+
+ if (shouldSend) {
+ event.preventDefault();
+ if (canSend) {
+ void onSendMessage();
+ }
+ }
+ }}
+ />
- {showHTMLVisualPromptButton ? (
-
-
+
+
+ {
+ setToolsMenuOpen(open);
+ if (!open) {
+ setToolsMenuHovered(false);
+ }
+ }}
+ >
+
-
-
- {htmlVisualPromptEnabled
- ? tComposer("htmlVisualPromptEnabled")
- : tComposer("htmlVisualPromptDisabled")}
-
-
- ) : null}
-
-
-
- {composerModeIndicator && ComposerModeIcon ? (
-
-
-
+
+ setHoveredTool("upload")}
+ onMouseLeave={() => setHoveredTool((prev) => (prev === "upload" ? null : prev))}
+ onSelect={(event) => {
+ event.preventDefault();
+ onSelectUploadTool();
+ }}
>
-
- {composerModeIndicator.label}
-
-
-
- {composerModeIndicator.intro} {composerModeIndicator.description}
-
-
- ) : null}
-
-
-
setIsVoiceHovered(true)}
- onMouseLeave={() => setIsVoiceHovered(false)}
- aria-label={sending ? tComposer("pauseGeneration") : hasDraftText ? tChat("send") : speechInput.active ? tComposer("cancelVoiceInput") : tComposer("voiceInput")}
- title={sending ? tComposer("pauseGeneration") : hasDraftText ? tChat("send") : speechInput.supported ? (speechInput.active ? tComposer("cancelVoiceInput") : tComposer("voiceInput")) : tComposer("voiceUnsupported")}
- >
- {sending ? (
-
- ) : speechInput.active ? (
-
- ) : hasDraftText ? (
-
+ {tComposer("uploadFile")}
+
+ setHoveredTool("screenshot")}
+ onMouseLeave={() => setHoveredTool((prev) => (prev === "screenshot" ? null : prev))}
+ onSelect={(event) => {
+ event.preventDefault();
+ onSelectScreenshotTool();
+ }}
+ >
+
+ {tComposer("screenshot")}
+
+
+
+
+ {!modelOptionPolicyDisabled ? (
+
- ) : (
-
- )}
-
-
-
+ ) : null}
+
+ {showHTMLVisualPromptButton ? (
+
+
+ onHTMLVisualPromptChange(!htmlVisualPromptEnabled)}
+ onMouseEnter={() => setIsBlocksHovered(true)}
+ onMouseLeave={() => setIsBlocksHovered(false)}
+ >
+
+
+
+
+ {htmlVisualPromptEnabled
+ ? tComposer("htmlVisualPromptEnabled")
+ : tComposer("htmlVisualPromptDisabled")}
+
+
+ ) : null}
+
+
+
+ {composerModeIndicator && ComposerModeIcon ? (
+
+
+
+
+ {composerModeIndicator.label}
+
+
+
+ {composerModeIndicator.intro} {composerModeIndicator.description}
+
+
+ ) : null}
+
+
+
setIsVoiceHovered(true)}
+ onMouseLeave={() => setIsVoiceHovered(false)}
+ aria-label={sending ? tComposer("pauseGeneration") : hasDraftText ? tChat("send") : speechInput.active ? tComposer("cancelVoiceInput") : tComposer("voiceInput")}
+ title={sending ? tComposer("pauseGeneration") : hasDraftText ? tChat("send") : speechInput.supported ? (speechInput.active ? tComposer("cancelVoiceInput") : tComposer("voiceInput")) : tComposer("voiceUnsupported")}
+ >
+ {sending ? (
+
+ ) : speechInput.active ? (
+
+ ) : hasDraftText ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
);
}
diff --git a/frontend/features/chat/components/sections/chat-model-config.tsx b/frontend/features/chat/components/sections/chat-model-config.tsx
index 94ef09f6..c4e56a7c 100644
--- a/frontend/features/chat/components/sections/chat-model-config.tsx
+++ b/frontend/features/chat/components/sections/chat-model-config.tsx
@@ -53,6 +53,7 @@ type VisualOption = {
placeholder?: string;
active: boolean;
editable: boolean;
+ locked?: boolean;
forcedFilterStatus?: ModelOptionFilterStatus;
};
@@ -75,6 +76,7 @@ type ChatModelConfigProps = {
options: ConversationOptions;
defaultOptions: ConversationOptions;
optionControls: ModelOptionControl[];
+ lockedOptionPaths: string[];
nativeToolKeys: string[];
nativeTools: ModelNativeToolConfig[];
modelOptionPolicy: ModelOptionPolicy | null;
@@ -696,6 +698,24 @@ function setOptionAtPath(options: ConversationOptions, path: string[], value: un
};
}
+function applyLockedDefaultOptions(
+ options: ConversationOptions,
+ defaults: ConversationOptions,
+ lockedPaths: string[],
+): ConversationOptions {
+ if (lockedPaths.length === 0) {
+ return options;
+ }
+ return lockedPaths.reduce((nextOptions, key) => {
+ const path = optionPathFromControl(key);
+ if (path.length === 0) {
+ return nextOptions;
+ }
+ const defaultValue = getOptionAtPath(defaults, path);
+ return defaultValue === undefined ? nextOptions : setOptionAtPath(nextOptions, path, defaultValue);
+ }, options);
+}
+
function visualOptionsFromOptions(
options: ConversationOptions,
policy: ModelOptionPolicy | null,
@@ -777,6 +797,7 @@ function resolveControlKind(control: ModelOptionControl): VisualOptionKind | und
function visualOptionsFromControls(
controls: ModelOptionControl[],
options: ConversationOptions,
+ defaultOptions: ConversationOptions = {},
): VisualOption[] {
return controls.flatMap((control): VisualOption[] => {
const path = optionPathFromControl(control.path);
@@ -784,8 +805,11 @@ function visualOptionsFromControls(
return [];
}
const key = optionPathKey(path);
- const value = resolveControlEditableValue(options, path);
- const active = hasOptionAtPath(options, path);
+ const hasLockedDefault = Boolean(control.locked && hasOptionAtPath(defaultOptions, path));
+ const value = hasLockedDefault
+ ? resolveControlEditableValue(defaultOptions, path)
+ : resolveControlEditableValue(options, path);
+ const active = hasOptionAtPath(options, path) || hasLockedDefault;
const selectValues = normalizeControlSelectValues(control.options);
return [{
key,
@@ -797,7 +821,8 @@ function visualOptionsFromControls(
kind: resolveControlKind(control),
selectValues,
placeholder: control.placeholder,
- editable: true,
+ editable: !control.locked,
+ locked: control.locked,
}];
});
}
@@ -956,6 +981,7 @@ export function ChatModelConfig({
options,
defaultOptions,
optionControls,
+ lockedOptionPaths,
nativeToolKeys,
nativeTools,
modelOptionPolicy,
@@ -989,8 +1015,8 @@ export function ChatModelConfig({
[nativeToolVisualOptions],
);
const configuredOptions = React.useMemo(
- () => visualOptionsFromControls(optionControls, optionsObject),
- [optionControls, optionsObject],
+ () => visualOptionsFromControls(optionControls, optionsObject, effectiveDefaultOptions),
+ [effectiveDefaultOptions, optionControls, optionsObject],
);
const configuredOptionKeys = React.useMemo(
() => new Set(configuredOptions.map((item) => item.key)),
@@ -1016,6 +1042,7 @@ export function ChatModelConfig({
() => [...configuredOptions, ...editableOptions],
[configuredOptions, editableOptions],
);
+ const lockedOptionPathSet = React.useMemo(() => new Set(lockedOptionPaths), [lockedOptionPaths]);
const hasRecognizedOptions = Boolean(nativeToolGroup) || visibleOptions.length > 0;
React.useEffect(() => {
@@ -1023,7 +1050,11 @@ export function ChatModelConfig({
}, [optionsObject]);
const openOptionsDialog = React.useCallback(() => {
- const sanitized = sanitizeConversationOptions(options);
+ const sanitized = applyLockedDefaultOptions(
+ sanitizeConversationOptions(options),
+ effectiveDefaultOptions,
+ lockedOptionPaths,
+ );
const hasVisualContent = hasVisualConfigurationContent({
nativeToolDefinitions,
optionControls,
@@ -1037,22 +1068,30 @@ export function ChatModelConfig({
setMobileView(hasVisualContent ? "visual" : "json");
setRestoredDefaultOptions(null);
setDialogOpen(true);
- }, [modelOptionPolicy, nativeToolDefinitions, optionControls, options, selectedProtocol]);
+ }, [effectiveDefaultOptions, lockedOptionPaths, modelOptionPolicy, nativeToolDefinitions, optionControls, options, selectedProtocol]);
const replaceOptionsDraft = React.useCallback((next: ConversationOptions) => {
- const sanitized = sanitizeConversationOptions(next);
+ const sanitized = applyLockedDefaultOptions(
+ sanitizeConversationOptions(next),
+ effectiveDefaultOptions,
+ lockedOptionPaths,
+ );
optionsObjectRef.current = sanitized;
setOptionsObject(sanitized);
setOptionsDraft(stringifyOptions(sanitized));
- }, []);
+ }, [effectiveDefaultOptions, lockedOptionPaths]);
const replaceRawOptionsDraft = React.useCallback((next: ConversationOptions) => {
const parsed = parseOptionsDraft(stringifyOptions(next));
- const nextOptions = parsed.rawOptions ?? next;
+ const nextOptions = applyLockedDefaultOptions(
+ parsed.rawOptions ?? next,
+ effectiveDefaultOptions,
+ lockedOptionPaths,
+ );
optionsObjectRef.current = nextOptions;
setOptionsObject(nextOptions);
- setOptionsDraft(stringifyOptions(next));
- }, []);
+ setOptionsDraft(stringifyOptions(nextOptions));
+ }, [effectiveDefaultOptions, lockedOptionPaths]);
const updateOptionValue = React.useCallback(
(path: string[], value: unknown) => {
@@ -1073,10 +1112,11 @@ export function ChatModelConfig({
const parsed = parseOptionsDraft(value);
if (parsed.rawOptions && parsed.options) {
- optionsObjectRef.current = parsed.rawOptions;
- setOptionsObject(parsed.rawOptions);
+ const nextOptions = applyLockedDefaultOptions(parsed.rawOptions, effectiveDefaultOptions, lockedOptionPaths);
+ optionsObjectRef.current = nextOptions;
+ setOptionsObject(nextOptions);
}
- }, []);
+ }, [effectiveDefaultOptions, lockedOptionPaths]);
const handleRestoreDefaultOptions = React.useCallback(async () => {
if (defaultRestorePending) {
@@ -1106,14 +1146,24 @@ export function ChatModelConfig({
toast.error(tComposer("saveFailed"));
return;
}
- if (JSON.stringify(parsed.options) === JSON.stringify(effectiveDefaultOptions)) {
+ const nextOptions = applyLockedDefaultOptions(
+ parsed.options,
+ effectiveDefaultOptions,
+ lockedOptionPaths,
+ );
+ if (JSON.stringify(nextOptions) !== JSON.stringify(parsed.options)) {
+ setOptionsDraft(stringifyOptions(nextOptions));
+ setOptionsObject(nextOptions);
+ optionsObjectRef.current = nextOptions;
+ }
+ if (JSON.stringify(nextOptions) === JSON.stringify(effectiveDefaultOptions)) {
onOptionsReset(effectiveDefaultOptions);
setDialogOpen(false);
return;
}
- onOptionsChange(parsed.options);
+ onOptionsChange(nextOptions);
setDialogOpen(false);
- }, [effectiveDefaultOptions, onOptionsChange, onOptionsReset, optionsDraft, tComposer]);
+ }, [effectiveDefaultOptions, lockedOptionPaths, onOptionsChange, onOptionsReset, optionsDraft, tComposer]);
const renderOptionsViewToggle = () => (
- {tComposer("ignoredHelp")}
+
+
{tComposer("notEnabledHelp")}
+
{tComposer("ignoredHelp")}
+
{tComposer("lockedHelp")}
+