mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(ui): centralize composer attachment and capability controls (#119650)
This commit is contained in:
committed by
GitHub
parent
36554c2820
commit
7e5c02ccfe
@@ -63,7 +63,7 @@ export class ChatAttachmentReadLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
export function isFileDrag(dataTransfer: DataTransfer | null): boolean {
|
||||
function isFileDrag(dataTransfer: DataTransfer | null): boolean {
|
||||
return Array.from(dataTransfer?.types ?? []).includes("Files");
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ const TEXT_ENTRY_INPUT_TYPES = new Set([
|
||||
// actually accept it; anywhere else (disabled/readonly inputs, non-text
|
||||
// controls like checkbox/range) an uncancelled URL drop navigates the app
|
||||
// away and discards unsent drafts.
|
||||
export function isEditableDropTarget(event: DragEvent): boolean {
|
||||
function isEditableDropTarget(event: DragEvent): boolean {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return false;
|
||||
@@ -100,7 +100,7 @@ function currentAttachments(props: ChatAttachmentControlsProps): ChatAttachment[
|
||||
return props.getAttachments?.() ?? props.attachments ?? [];
|
||||
}
|
||||
|
||||
export function clickComposerInput(target: HTMLElement, selector: string) {
|
||||
function clickComposerInput(target: HTMLElement, selector: string) {
|
||||
target.closest("details")?.removeAttribute("open");
|
||||
target
|
||||
.closest(".agent-chat__composer-shell, .new-session-page__composer")
|
||||
@@ -363,7 +363,7 @@ function handleChatAttachmentFileSelect(e: Event, props: ChatAttachmentControlsP
|
||||
void appendAttachmentFiles(files, props);
|
||||
}
|
||||
|
||||
export function handleChatAttachmentDrop(e: DragEvent, props: ChatAttachmentControlsProps) {
|
||||
function handleChatAttachmentDrop(e: DragEvent, props: ChatAttachmentControlsProps) {
|
||||
e.preventDefault();
|
||||
void appendAttachmentFiles([...(e.dataTransfer?.files ?? [])], props);
|
||||
}
|
||||
@@ -372,6 +372,8 @@ type ChatAttachmentDropProps = ChatAttachmentControlsProps & {
|
||||
canCompose: boolean;
|
||||
};
|
||||
|
||||
// Both composers share balanced nested drag state and cancel non-editable
|
||||
// text/URL drops so disabled surfaces cannot navigate away from a draft.
|
||||
export function createChatAttachmentDropHandlers(props: ChatAttachmentDropProps) {
|
||||
let depth = 0;
|
||||
const setActive = (event: DragEvent, active: boolean) => {
|
||||
@@ -432,42 +434,74 @@ export function createChatAttachmentDropHandlers(props: ChatAttachmentDropProps)
|
||||
|
||||
export function renderChatAttachmentInputs(props: ChatAttachmentControlsProps) {
|
||||
return html`
|
||||
<input
|
||||
type="file"
|
||||
accept=${CHAT_ATTACHMENT_ACCEPT}
|
||||
multiple
|
||||
class="agent-chat__file-input"
|
||||
?disabled=${props.disabled}
|
||||
@change=${(event: Event) => {
|
||||
if (!props.disabled) {
|
||||
handleChatAttachmentFileSelect(event, props);
|
||||
${(["file", "photo", "camera"] as const).map(
|
||||
(kind) => html`
|
||||
<input
|
||||
type="file"
|
||||
accept=${kind === "file" ? CHAT_ATTACHMENT_ACCEPT : "image/*"}
|
||||
?multiple=${kind !== "camera"}
|
||||
capture=${kind === "camera" ? "environment" : nothing}
|
||||
class=${`agent-chat__${kind}-input`}
|
||||
?disabled=${props.disabled}
|
||||
@change=${(event: Event) => {
|
||||
if (!props.disabled) {
|
||||
handleChatAttachmentFileSelect(event, props);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
export function handleChatAttachmentMenuSelection(
|
||||
event: CustomEvent<{ item: { value?: string } }>,
|
||||
): boolean {
|
||||
const value = event.detail.item.value;
|
||||
if (value !== "camera" && value !== "photo" && value !== "file") {
|
||||
return false;
|
||||
}
|
||||
clickComposerInput(event.currentTarget as HTMLElement, `.agent-chat__${value}-input`);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function renderChatAttachmentMenuTrigger(disabled: boolean | undefined) {
|
||||
return html`
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class="agent-chat__input-btn agent-chat__input-btn--attach"
|
||||
aria-label=${t("chat.composer.addAttachment")}
|
||||
?disabled=${disabled}
|
||||
title=${t("chat.composer.addAttachment")}
|
||||
@pointerdown=${(event: PointerEvent) => {
|
||||
const composer = (event.currentTarget as HTMLElement)
|
||||
.closest(".agent-chat__composer-shell")
|
||||
?.querySelector("textarea");
|
||||
if (document.activeElement === composer) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
class="agent-chat__photo-input"
|
||||
?disabled=${props.disabled}
|
||||
@change=${(event: Event) => {
|
||||
if (!props.disabled) {
|
||||
handleChatAttachmentFileSelect(event, props);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
class="agent-chat__camera-input"
|
||||
?disabled=${props.disabled}
|
||||
@change=${(event: Event) => {
|
||||
if (!props.disabled) {
|
||||
handleChatAttachmentFileSelect(event, props);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
>
|
||||
${icons.plus}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderChatAttachmentMenuOptions(fileIcon = icons.folder) {
|
||||
return html`
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="camera">
|
||||
<span slot="icon" aria-hidden="true">${icons.camera}</span>
|
||||
<span>${t("chat.composer.takePhoto")}</span>
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="photo">
|
||||
<span slot="icon" aria-hidden="true">${icons.image}</span>
|
||||
<span>${t("chat.composer.attachPhoto")}</span>
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="file">
|
||||
<span slot="icon" aria-hidden="true">${fileIcon}</span>
|
||||
<span>${t("chat.composer.attachFileOption")}</span>
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -477,51 +511,9 @@ export function renderChatAttachmentMenu(props: ChatAttachmentControlsProps) {
|
||||
class="agent-chat__attach-menu"
|
||||
placement="top-start"
|
||||
aria-label=${t("chat.composer.addAttachment")}
|
||||
@wa-select=${(event: CustomEvent<{ item: { value?: string } }>) => {
|
||||
const menu = event.currentTarget as HTMLElement;
|
||||
const selector =
|
||||
event.detail.item.value === "camera"
|
||||
? ".agent-chat__camera-input"
|
||||
: event.detail.item.value === "photo"
|
||||
? ".agent-chat__photo-input"
|
||||
: event.detail.item.value === "file"
|
||||
? ".agent-chat__file-input"
|
||||
: null;
|
||||
if (selector) {
|
||||
clickComposerInput(menu, selector);
|
||||
}
|
||||
}}
|
||||
@wa-select=${handleChatAttachmentMenuSelection}
|
||||
>
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class="agent-chat__input-btn agent-chat__input-btn--attach"
|
||||
aria-label=${t("chat.composer.addAttachment")}
|
||||
?disabled=${props.disabled}
|
||||
title=${t("chat.composer.addAttachment")}
|
||||
@pointerdown=${(event: PointerEvent) => {
|
||||
const composer = (event.currentTarget as HTMLElement)
|
||||
.closest(".agent-chat__composer-shell")
|
||||
?.querySelector("textarea");
|
||||
if (document.activeElement === composer) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${icons.plus}
|
||||
</button>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="camera">
|
||||
<span slot="icon" aria-hidden="true">${icons.camera}</span>
|
||||
<span>${t("chat.composer.takePhoto")}</span>
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="photo">
|
||||
<span slot="icon" aria-hidden="true">${icons.image}</span>
|
||||
<span>${t("chat.composer.attachPhoto")}</span>
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="file">
|
||||
<span slot="icon" aria-hidden="true">${icons.folder}</span>
|
||||
<span>${t("chat.composer.attachFileOption")}</span>
|
||||
</wa-dropdown-item>
|
||||
${renderChatAttachmentMenuTrigger(props.disabled)} ${renderChatAttachmentMenuOptions()}
|
||||
</wa-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ import {
|
||||
readOwnEntry,
|
||||
resolveToolOverrideState,
|
||||
} from "../../../lib/sessions/tool-overrides.ts";
|
||||
import { clickComposerInput, type ChatAttachmentControlsProps } from "./chat-attachments.ts";
|
||||
import {
|
||||
handleChatAttachmentMenuSelection,
|
||||
renderChatAttachmentMenuOptions,
|
||||
renderChatAttachmentMenuTrigger,
|
||||
type ChatAttachmentControlsProps,
|
||||
} from "./chat-attachments.ts";
|
||||
|
||||
export type ChatComposerPlusMenuView = "root" | "skills" | "connectors" | `tools:${string}`;
|
||||
|
||||
@@ -86,6 +91,38 @@ function renderBackRow() {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCapabilityToggleRow(options: {
|
||||
value: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
title: string | null | undefined;
|
||||
note?: TemplateResult | typeof nothing;
|
||||
}) {
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
class="agent-chat__capability-menu-item agent-chat__capability-menu-toggle"
|
||||
value=${options.value}
|
||||
?disabled=${options.disabled}
|
||||
title=${options.title ?? ""}
|
||||
>
|
||||
<span class="agent-chat__capability-menu-label">
|
||||
<span>${options.label}</span>
|
||||
${options.note ?? nothing}
|
||||
</span>
|
||||
<wa-switch
|
||||
slot="details"
|
||||
class="agent-chat__capability-menu-switch"
|
||||
size="s"
|
||||
tabindex="-1"
|
||||
.checked=${options.checked}
|
||||
?disabled=${options.disabled}
|
||||
aria-label=${options.label}
|
||||
></wa-switch>
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRootView(props: ChatComposerPlusMenuProps) {
|
||||
const connectorCount = props.mcpServers.filter((server) =>
|
||||
resolveToolOverrideState(
|
||||
@@ -99,20 +136,7 @@ function renderRootView(props: ChatComposerPlusMenuProps) {
|
||||
props.webSearchBaseEnabled,
|
||||
props.toolOverrides?.webSearch,
|
||||
);
|
||||
const attachments = html`
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="camera">
|
||||
<span slot="icon" aria-hidden="true">${icons.camera}</span>
|
||||
<span>${t("chat.composer.takePhoto")}</span>
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="photo">
|
||||
<span slot="icon" aria-hidden="true">${icons.image}</span>
|
||||
<span>${t("chat.composer.attachPhoto")}</span>
|
||||
</wa-dropdown-item>
|
||||
<wa-dropdown-item class="agent-chat__attach-menu-option" value="file">
|
||||
<span slot="icon" aria-hidden="true">${icons.paperclip}</span>
|
||||
<span>${t("chat.composer.attachFileOption")}</span>
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
const attachments = renderChatAttachmentMenuOptions(icons.paperclip);
|
||||
if (!props.showCapabilities) {
|
||||
return attachments;
|
||||
}
|
||||
@@ -183,42 +207,22 @@ function renderSkillView(props: ChatComposerPlusMenuProps) {
|
||||
${t("chat.composer.menu.noSkills")}
|
||||
</div>`
|
||||
: props.skills.map((skill, index) => {
|
||||
const rowDisabled = skill.missingDeps || skill.blocked || disabledReason !== null;
|
||||
const title = skill.missingDeps
|
||||
? t("chat.composer.menu.depsMissing")
|
||||
: skill.blocked
|
||||
? t("chat.composer.menu.skillBlocked")
|
||||
: disabledReason;
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
class="agent-chat__capability-menu-item agent-chat__capability-menu-toggle"
|
||||
value=${`skill:${index}`}
|
||||
?disabled=${rowDisabled}
|
||||
title=${title ?? ""}
|
||||
>
|
||||
<span class="agent-chat__capability-menu-label">
|
||||
<span>${skill.name}</span>
|
||||
${skill.missingDeps
|
||||
? html`<span class="agent-chat__capability-menu-note"
|
||||
>${t("chat.composer.menu.depsMissing")}</span
|
||||
>`
|
||||
: skill.blocked
|
||||
? html`<span class="agent-chat__capability-menu-note"
|
||||
>${t("chat.composer.menu.skillBlocked")}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>
|
||||
<wa-switch
|
||||
slot="details"
|
||||
class="agent-chat__capability-menu-switch"
|
||||
size="s"
|
||||
tabindex="-1"
|
||||
.checked=${skill.enabled}
|
||||
?disabled=${rowDisabled}
|
||||
aria-label=${skill.name}
|
||||
></wa-switch>
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
return renderCapabilityToggleRow({
|
||||
value: `skill:${index}`,
|
||||
label: skill.name,
|
||||
checked: skill.enabled,
|
||||
disabled: skill.missingDeps || skill.blocked || disabledReason !== null,
|
||||
title,
|
||||
note:
|
||||
skill.missingDeps || skill.blocked
|
||||
? html`<span class="agent-chat__capability-menu-note">${title}</span>`
|
||||
: nothing,
|
||||
});
|
||||
});
|
||||
return html`
|
||||
${renderBackRow()} ${rows} ${menuDivider()}
|
||||
@@ -239,33 +243,21 @@ function renderConnectorView(props: ChatComposerPlusMenuProps) {
|
||||
const override = readOwnEntry(props.toolOverrides?.mcpServers, server.name);
|
||||
const enabled = resolveToolOverrideState(server.enabled, override);
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
class="agent-chat__capability-menu-item agent-chat__capability-menu-toggle"
|
||||
value=${`connector:${index}`}
|
||||
?disabled=${disabledReason !== null}
|
||||
title=${disabledReason ?? ""}
|
||||
>
|
||||
<span class="agent-chat__capability-menu-label">
|
||||
<span>${server.name}</span>
|
||||
<span class="agent-chat__capability-menu-note">
|
||||
${enabled ? t("common.enabled") : t("common.disabled")}
|
||||
${override !== undefined
|
||||
? html`<span class="agent-chat__capability-menu-session-tag"
|
||||
>${t("chat.composer.menu.sessionTag")}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>
|
||||
</span>
|
||||
<wa-switch
|
||||
slot="details"
|
||||
class="agent-chat__capability-menu-switch"
|
||||
size="s"
|
||||
tabindex="-1"
|
||||
.checked=${enabled}
|
||||
?disabled=${disabledReason !== null}
|
||||
aria-label=${server.name}
|
||||
></wa-switch>
|
||||
</wa-dropdown-item>
|
||||
${renderCapabilityToggleRow({
|
||||
value: `connector:${index}`,
|
||||
label: server.name,
|
||||
checked: enabled,
|
||||
disabled: disabledReason !== null,
|
||||
title: disabledReason,
|
||||
note: html`<span class="agent-chat__capability-menu-note">
|
||||
${enabled ? t("common.enabled") : t("common.disabled")}
|
||||
${override !== undefined
|
||||
? html`<span class="agent-chat__capability-menu-session-tag"
|
||||
>${t("chat.composer.menu.sessionTag")}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>`,
|
||||
})}
|
||||
${props.onOpenToolAccess
|
||||
? html`<wa-dropdown-item
|
||||
class="agent-chat__capability-menu-item agent-chat__capability-menu-subrow"
|
||||
@@ -309,7 +301,7 @@ function renderConnectorView(props: ChatComposerPlusMenuProps) {
|
||||
function toolsForServer(
|
||||
result: ToolsEffectiveResult | null,
|
||||
serverName: string,
|
||||
): ToolsEffectiveEntry[] {
|
||||
): (ToolsEffectiveEntry & { mcpToolName: string })[] {
|
||||
return (result?.groups ?? [])
|
||||
.flatMap((group) => group.tools)
|
||||
.filter(
|
||||
@@ -376,30 +368,17 @@ function renderToolAccessView(props: ChatComposerPlusMenuProps, serverName: stri
|
||||
const rawToolName = tool.mcpToolName;
|
||||
const label = tool.label?.trim();
|
||||
const denied = isToolDenied(props, tool);
|
||||
return html`
|
||||
<wa-dropdown-item
|
||||
class="agent-chat__capability-menu-item agent-chat__capability-menu-toggle"
|
||||
value=${`mcp-tool:${index}`}
|
||||
?disabled=${props.toolAccessMutationBlockedReason !== null}
|
||||
title=${props.toolAccessMutationBlockedReason ?? ""}
|
||||
>
|
||||
<span class="agent-chat__capability-menu-label">
|
||||
<span>${rawToolName}</span>
|
||||
${label && label !== rawToolName
|
||||
? html`<span class="agent-chat__capability-menu-note">${label}</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
<wa-switch
|
||||
slot="details"
|
||||
class="agent-chat__capability-menu-switch"
|
||||
size="s"
|
||||
tabindex="-1"
|
||||
.checked=${!denied}
|
||||
?disabled=${props.toolAccessMutationBlockedReason !== null}
|
||||
aria-label=${rawToolName}
|
||||
></wa-switch>
|
||||
</wa-dropdown-item>
|
||||
`;
|
||||
return renderCapabilityToggleRow({
|
||||
value: `mcp-tool:${index}`,
|
||||
label: rawToolName,
|
||||
checked: !denied,
|
||||
disabled: props.toolAccessMutationBlockedReason !== null,
|
||||
title: props.toolAccessMutationBlockedReason,
|
||||
note:
|
||||
label && label !== rawToolName
|
||||
? html`<span class="agent-chat__capability-menu-note">${label}</span>`
|
||||
: nothing,
|
||||
});
|
||||
});
|
||||
return html`
|
||||
${renderBackRow()}
|
||||
@@ -420,6 +399,9 @@ function handleMenuSelection(
|
||||
props: ChatComposerPlusMenuProps,
|
||||
) {
|
||||
const value = event.detail.item.value ?? "";
|
||||
if (handleChatAttachmentMenuSelection(event)) {
|
||||
return;
|
||||
}
|
||||
const menu = event.currentTarget as HTMLElement;
|
||||
const changeView = (view: ChatComposerPlusMenuView) => {
|
||||
props.onViewChange(view);
|
||||
@@ -427,10 +409,6 @@ function handleMenuSelection(
|
||||
menu.querySelector<HTMLElement>("wa-dropdown-item:not([disabled])")?.focus(),
|
||||
);
|
||||
};
|
||||
if (value === "camera" || value === "photo" || value === "file") {
|
||||
clickComposerInput(menu, `.agent-chat__${value === "file" ? "file" : value}-input`);
|
||||
return;
|
||||
}
|
||||
if (value === "back") {
|
||||
event.preventDefault();
|
||||
changeView(props.view.startsWith("tools:") ? "connectors" : "root");
|
||||
@@ -570,25 +548,7 @@ export function renderChatComposerPlusMenu(props: ChatComposerPlusMenuProps) {
|
||||
}}
|
||||
data-view=${view}
|
||||
>
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class="agent-chat__input-btn agent-chat__input-btn--attach"
|
||||
aria-label=${t("chat.composer.addAttachment")}
|
||||
?disabled=${props.disabled}
|
||||
title=${t("chat.composer.addAttachment")}
|
||||
@pointerdown=${(event: PointerEvent) => {
|
||||
const composer = (event.currentTarget as HTMLElement)
|
||||
.closest(".agent-chat__composer-shell")
|
||||
?.querySelector("textarea");
|
||||
if (document.activeElement === composer) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${icons.plus}
|
||||
</button>
|
||||
${content}
|
||||
${renderChatAttachmentMenuTrigger(props.disabled)} ${content}
|
||||
</wa-dropdown>
|
||||
${props.addServerDialog ?? nothing}
|
||||
`;
|
||||
|
||||
@@ -5,10 +5,8 @@ import "../../components/tooltip.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import type { ChatAttachment } from "../../lib/chat/chat-types.ts";
|
||||
import {
|
||||
handleChatAttachmentDrop,
|
||||
createChatAttachmentDropHandlers,
|
||||
handleChatAttachmentPaste,
|
||||
isEditableDropTarget,
|
||||
isFileDrag,
|
||||
renderAttachmentPreview,
|
||||
renderChatAttachmentInputs,
|
||||
renderChatAttachmentMenu,
|
||||
@@ -144,69 +142,18 @@ function renderNewSessionComposer(options: NewSessionComposerOptions) {
|
||||
onPendingReadsChange: options.onPendingReadsChange,
|
||||
readSignal: options.readSignal,
|
||||
};
|
||||
const enabled = !options.submitting && !options.messageLocked;
|
||||
const attachmentDropHandlers = createChatAttachmentDropHandlers({
|
||||
...attachmentProps,
|
||||
canCompose: !options.submitting && !options.messageLocked,
|
||||
});
|
||||
options.textareaController.syncDraft(options.message);
|
||||
// Nested dragenter/dragleave events must stay balanced so crossing composer
|
||||
// children does not flicker the file drop affordance.
|
||||
let attachmentDragDepth = 0;
|
||||
const setAttachmentDropActive = (event: DragEvent, active: boolean) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (active) {
|
||||
if (!enabled || !isFileDrag(event.dataTransfer)) {
|
||||
return;
|
||||
}
|
||||
attachmentDragDepth += 1;
|
||||
} else {
|
||||
attachmentDragDepth = Math.max(0, attachmentDragDepth - 1);
|
||||
}
|
||||
target.toggleAttribute("data-attachment-drop-active", attachmentDragDepth > 0);
|
||||
};
|
||||
const clearAttachmentDropActive = (event: DragEvent) => {
|
||||
attachmentDragDepth = 0;
|
||||
const target = event.currentTarget;
|
||||
if (target instanceof HTMLElement) {
|
||||
target.removeAttribute("data-attachment-drop-active");
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<div
|
||||
class="agent-chat__composer-shell new-session-page__composer"
|
||||
@drop=${(event: DragEvent) => {
|
||||
// Text/URL drops stay native only inside the textarea; elsewhere they
|
||||
// are cancelled so a dropped link cannot navigate the app away. File
|
||||
// drops are cancelled even while disabled for the same reason.
|
||||
if (!isFileDrag(event.dataTransfer)) {
|
||||
if (!isEditableDropTarget(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
clearAttachmentDropActive(event);
|
||||
if (enabled) {
|
||||
handleChatAttachmentDrop(event, attachmentProps);
|
||||
}
|
||||
}}
|
||||
@dragenter=${(event: DragEvent) => setAttachmentDropActive(event, true)}
|
||||
@dragleave=${(event: DragEvent) => setAttachmentDropActive(event, false)}
|
||||
@dragover=${(event: DragEvent) => {
|
||||
if (!isFileDrag(event.dataTransfer)) {
|
||||
if (!isEditableDropTarget(event)) {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "none";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = enabled ? "copy" : "none";
|
||||
}
|
||||
}}
|
||||
@drop=${attachmentDropHandlers.onDrop}
|
||||
@dragenter=${attachmentDropHandlers.onDragenter}
|
||||
@dragleave=${attachmentDropHandlers.onDragleave}
|
||||
@dragover=${attachmentDropHandlers.onDragover}
|
||||
>
|
||||
<div class="agent-chat__input">
|
||||
${renderChatAttachmentInputs(attachmentProps)} ${renderAttachmentPreview(attachmentProps)}
|
||||
|
||||
Reference in New Issue
Block a user