fix git bugs.
parent
a774c398e8
commit
9f98d3f68d
|
|
@ -1,5 +1,5 @@
|
|||
fastapi==0.110.0
|
||||
uvicorn==0.27.1
|
||||
uvicorn[standard]==0.27.1
|
||||
docker==7.0.0
|
||||
sqlmodel==0.0.16
|
||||
pydantic==2.6.3
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip
|
|||
const tooltipId = useId();
|
||||
const wrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const bubbleRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [requestedVisible, setRequestedVisible] = useState(false);
|
||||
const [layout, setLayout] = useState<TooltipLayout | null>(null);
|
||||
|
||||
const child = useMemo(() => {
|
||||
|
|
@ -44,6 +44,7 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip
|
|||
return isValidElement(first) ? (first as ReactElement<{ 'aria-describedby'?: string; disabled?: boolean }>) : null;
|
||||
}, [children]);
|
||||
const childDisabled = Boolean(child?.props.disabled);
|
||||
const visible = requestedVisible && !childDisabled;
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
const wrap = wrapRef.current;
|
||||
|
|
@ -95,20 +96,17 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip
|
|||
}, [side]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!visible) {
|
||||
setLayout(null);
|
||||
return;
|
||||
}
|
||||
if (!visible) return;
|
||||
updatePosition();
|
||||
}, [updatePosition, visible, text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
const handleWindowChange = () => updatePosition();
|
||||
const handleDismiss = () => setVisible(false);
|
||||
const handleDismiss = () => setRequestedVisible(false);
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setVisible(false);
|
||||
setRequestedVisible(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', handleWindowChange, true);
|
||||
|
|
@ -125,12 +123,6 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip
|
|||
};
|
||||
}, [updatePosition, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (childDisabled) {
|
||||
setVisible(false);
|
||||
}
|
||||
}, [childDisabled]);
|
||||
|
||||
if (!text) return <>{children}</>;
|
||||
|
||||
const enhancedChild = child
|
||||
|
|
@ -144,19 +136,23 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip
|
|||
<span
|
||||
ref={wrapRef}
|
||||
className="lucent-tooltip-wrap"
|
||||
onMouseEnter={() => setVisible(true)}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
onPointerDownCapture={() => setVisible(false)}
|
||||
onClickCapture={() => setVisible(false)}
|
||||
onFocusCapture={() => setVisible(true)}
|
||||
onMouseEnter={() => {
|
||||
if (!childDisabled) setRequestedVisible(true);
|
||||
}}
|
||||
onMouseLeave={() => setRequestedVisible(false)}
|
||||
onPointerDownCapture={() => setRequestedVisible(false)}
|
||||
onClickCapture={() => setRequestedVisible(false)}
|
||||
onFocusCapture={() => {
|
||||
if (!childDisabled) setRequestedVisible(true);
|
||||
}}
|
||||
onKeyDownCapture={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Escape') {
|
||||
setVisible(false);
|
||||
setRequestedVisible(false);
|
||||
}
|
||||
}}
|
||||
onBlurCapture={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
||||
setVisible(false);
|
||||
setRequestedVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,15 @@ interface SkillMarketInstallModalProps {
|
|||
}
|
||||
|
||||
export function SkillMarketInstallModal({
|
||||
isZh,
|
||||
open,
|
||||
...props
|
||||
}: SkillMarketInstallModalProps) {
|
||||
if (!open) return null;
|
||||
return <SkillMarketInstallModalContent {...props} />;
|
||||
}
|
||||
|
||||
function SkillMarketInstallModalContent({
|
||||
isZh,
|
||||
items,
|
||||
loading,
|
||||
installingId,
|
||||
|
|
@ -30,24 +37,24 @@ export function SkillMarketInstallModal({
|
|||
onRefresh,
|
||||
onInstall,
|
||||
formatBytes,
|
||||
}: SkillMarketInstallModalProps) {
|
||||
}: Omit<SkillMarketInstallModalProps, 'open'>) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(() => readCachedPlatformPageSize(10));
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setSearch('');
|
||||
setPage(1);
|
||||
let cancelled = false;
|
||||
void onRefresh();
|
||||
void (async () => {
|
||||
setPageSize(await fetchPreferredPlatformPageSize(10));
|
||||
const nextPageSize = await fetchPreferredPlatformPageSize(10);
|
||||
if (!cancelled) {
|
||||
setPageSize(nextPageSize);
|
||||
}
|
||||
})();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [search, pageSize]);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [onRefresh]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const keyword = search.trim().toLowerCase();
|
||||
|
|
@ -66,8 +73,6 @@ export function SkillMarketInstallModal({
|
|||
[currentPage, filteredItems, pageSize],
|
||||
);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<ModalCardShell
|
||||
cardClassName="modal-wide platform-modal skill-market-browser-shell"
|
||||
|
|
@ -89,8 +94,14 @@ export function SkillMarketInstallModal({
|
|||
<ProtectedSearchInput
|
||||
className="platform-searchbar skill-market-search"
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
onClear={() => setSearch('')}
|
||||
onChange={(value) => {
|
||||
setSearch(value);
|
||||
setPage(1);
|
||||
}}
|
||||
onClear={() => {
|
||||
setSearch('');
|
||||
setPage(1);
|
||||
}}
|
||||
autoFocus
|
||||
debounceMs={120}
|
||||
placeholder={isZh ? '搜索技能、标识或 ZIP 文件名...' : 'Search skills, keys, or ZIP filenames...'}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,43 @@ interface PromptApi {
|
|||
confirm: (options: ConfirmOptions) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ChannelManagerLabels {
|
||||
channelSaved: string;
|
||||
channelSaveFail: string;
|
||||
channelAddFail: string;
|
||||
channelDeleted: string;
|
||||
channelDeleteConfirm: (channelType: string) => string;
|
||||
channelDeleteFail: string;
|
||||
channels: string;
|
||||
}
|
||||
|
||||
export interface GlobalDeliveryState {
|
||||
sendProgress: boolean;
|
||||
sendToolHints: boolean;
|
||||
}
|
||||
|
||||
interface ApiErrorDetail {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
function resolveApiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (axios.isAxiosError<ApiErrorDetail>(error)) {
|
||||
const detail = String(error.response?.data?.detail || '').trim();
|
||||
if (detail) return detail;
|
||||
const message = String(error.message || '').trim();
|
||||
if (message) return message;
|
||||
} else if (error instanceof Error) {
|
||||
const message = String(error.message || '').trim();
|
||||
if (message) return message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
interface ChannelManagerDeps extends PromptApi {
|
||||
selectedBotId: string;
|
||||
selectedBotDockerStatus: string;
|
||||
t: any;
|
||||
currentGlobalDelivery: { sendProgress: boolean; sendToolHints: boolean };
|
||||
t: ChannelManagerLabels;
|
||||
currentGlobalDelivery: GlobalDeliveryState;
|
||||
addableChannelTypes: ChannelType[];
|
||||
currentNewChannelDraft: BotChannel;
|
||||
refresh: () => Promise<void>;
|
||||
|
|
@ -42,9 +74,7 @@ interface ChannelManagerDeps extends PromptApi {
|
|||
setNewChannelDraft: (value: BotChannel | ((prev: BotChannel) => BotChannel)) => void;
|
||||
setIsSavingChannel: (value: boolean) => void;
|
||||
setGlobalDelivery: (
|
||||
value:
|
||||
| { sendProgress: boolean; sendToolHints: boolean }
|
||||
| ((prev: { sendProgress: boolean; sendToolHints: boolean }) => { sendProgress: boolean; sendToolHints: boolean })
|
||||
value: GlobalDeliveryState | ((prev: GlobalDeliveryState) => GlobalDeliveryState)
|
||||
) => void;
|
||||
setIsSavingGlobalDelivery: (value: boolean) => void;
|
||||
}
|
||||
|
|
@ -163,8 +193,8 @@ export function createChannelManager({
|
|||
});
|
||||
await loadChannels(selectedBotId);
|
||||
notify(t.channelSaved, { tone: 'success' });
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.detail || t.channelSaveFail;
|
||||
} catch (error: unknown) {
|
||||
const message = resolveApiErrorMessage(error, t.channelSaveFail);
|
||||
notify(message, { tone: 'error' });
|
||||
} finally {
|
||||
setIsSavingChannel(false);
|
||||
|
|
@ -188,8 +218,8 @@ export function createChannelManager({
|
|||
await loadChannels(selectedBotId);
|
||||
setNewChannelPanelOpen(false);
|
||||
resetNewChannelDraft();
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.detail || t.channelAddFail;
|
||||
} catch (error: unknown) {
|
||||
const message = resolveApiErrorMessage(error, t.channelAddFail);
|
||||
notify(message, { tone: 'error' });
|
||||
} finally {
|
||||
setIsSavingChannel(false);
|
||||
|
|
@ -209,8 +239,8 @@ export function createChannelManager({
|
|||
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/channels/${channel.id}`);
|
||||
await loadChannels(selectedBotId);
|
||||
notify(t.channelDeleted, { tone: 'success' });
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.detail || t.channelDeleteFail;
|
||||
} catch (error: unknown) {
|
||||
const message = resolveApiErrorMessage(error, t.channelDeleteFail);
|
||||
notify(message, { tone: 'error' });
|
||||
} finally {
|
||||
setIsSavingChannel(false);
|
||||
|
|
@ -235,8 +265,8 @@ export function createChannelManager({
|
|||
}
|
||||
await refresh();
|
||||
notify(t.channelSaved, { tone: 'success' });
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.detail || t.channelSaveFail;
|
||||
} catch (error: unknown) {
|
||||
const message = resolveApiErrorMessage(error, t.channelSaveFail);
|
||||
notify(message, { tone: 'error' });
|
||||
} finally {
|
||||
setIsSavingGlobalDelivery(false);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useDashboardSupportData } from './useDashboardSupportData';
|
|||
import { useDashboardSystemDefaults } from './useDashboardSystemDefaults';
|
||||
import { useDashboardTemplateManager } from './useDashboardTemplateManager';
|
||||
import { useDashboardVoiceInput } from './useDashboardVoiceInput';
|
||||
import { loadComposerDraft } from '../utils';
|
||||
|
||||
export function useBotDashboardModule({
|
||||
forcedBotId,
|
||||
|
|
@ -380,6 +381,7 @@ export function useBotDashboardModule({
|
|||
selectedBotDockerStatus: selectedBot?.docker_status || '',
|
||||
workspaceDownloadExtensions,
|
||||
refreshAttachmentPolicy,
|
||||
restorePendingAttachments: (botId) => loadComposerDraft(botId)?.attachments || [],
|
||||
notify,
|
||||
t,
|
||||
isZh,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from 'react';
|
||||
|
||||
import { channelsEn } from '../../../i18n/channels.en';
|
||||
import type { BotState } from '../../../types/bot';
|
||||
import { optionalChannelTypes } from '../constants';
|
||||
import { createChannelManager } from '../config-managers/channelManager';
|
||||
import { createChannelManager, type ChannelManagerLabels, type GlobalDeliveryState } from '../config-managers/channelManager';
|
||||
import type { BotChannel, WeixinLoginStatus } from '../types';
|
||||
|
||||
type PromptTone = 'info' | 'success' | 'warning' | 'error';
|
||||
|
|
@ -29,13 +31,28 @@ interface UseDashboardChannelConfigOptions {
|
|||
passwordToggleLabels: { show: string; hide: string };
|
||||
refresh: () => Promise<void>;
|
||||
reloginWeixin: () => Promise<void>;
|
||||
selectedBot?: any;
|
||||
selectedBot?: Pick<BotState, 'id' | 'docker_status' | 'send_progress' | 'send_tool_hints'> | null;
|
||||
selectedBotId: string;
|
||||
t: any;
|
||||
lc: any;
|
||||
t: ChannelManagerLabels & { cancel: string; close: string };
|
||||
lc: typeof channelsEn;
|
||||
weixinLoginStatus: WeixinLoginStatus | null;
|
||||
}
|
||||
|
||||
const EMPTY_GLOBAL_DELIVERY: GlobalDeliveryState = {
|
||||
sendProgress: false,
|
||||
sendToolHints: false,
|
||||
};
|
||||
|
||||
function readBotGlobalDelivery(
|
||||
bot?: Pick<BotState, 'send_progress' | 'send_tool_hints'> | null,
|
||||
): GlobalDeliveryState {
|
||||
if (!bot) return EMPTY_GLOBAL_DELIVERY;
|
||||
return {
|
||||
sendProgress: Boolean(bot.send_progress),
|
||||
sendToolHints: Boolean(bot.send_tool_hints),
|
||||
};
|
||||
}
|
||||
|
||||
export function useDashboardChannelConfig({
|
||||
closeRuntimeMenu,
|
||||
confirm,
|
||||
|
|
@ -69,16 +86,27 @@ export function useDashboardChannelConfig({
|
|||
});
|
||||
const [isSavingChannel, setIsSavingChannel] = useState(false);
|
||||
const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false);
|
||||
const [globalDelivery, setGlobalDelivery] = useState<{ sendProgress: boolean; sendToolHints: boolean }>({
|
||||
sendProgress: false,
|
||||
sendToolHints: false,
|
||||
});
|
||||
const [globalDeliveryDraftByBot, setGlobalDeliveryDraftByBot] = useState<Record<string, GlobalDeliveryState>>({});
|
||||
|
||||
const addableChannelTypes = useMemo(() => {
|
||||
const exists = new Set(channels.map((channel) => String(channel.channel_type).toLowerCase()));
|
||||
return optionalChannelTypes.filter((type) => !exists.has(type));
|
||||
}, [channels]);
|
||||
|
||||
const globalDelivery = useMemo(() => {
|
||||
if (!selectedBotId || !selectedBot) return EMPTY_GLOBAL_DELIVERY;
|
||||
return globalDeliveryDraftByBot[selectedBotId] ?? readBotGlobalDelivery(selectedBot);
|
||||
}, [globalDeliveryDraftByBot, selectedBot, selectedBotId]);
|
||||
|
||||
const setGlobalDelivery = useCallback((value: SetStateAction<GlobalDeliveryState>) => {
|
||||
if (!selectedBotId) return;
|
||||
setGlobalDeliveryDraftByBot((prev) => {
|
||||
const currentValue = prev[selectedBotId] ?? readBotGlobalDelivery(selectedBot);
|
||||
const nextValue = typeof value === 'function' ? value(currentValue) : value;
|
||||
return { ...prev, [selectedBotId]: nextValue };
|
||||
});
|
||||
}, [selectedBot, selectedBotId]);
|
||||
|
||||
const {
|
||||
resetNewChannelDraft,
|
||||
channelDraftUiKey,
|
||||
|
|
@ -112,17 +140,6 @@ export function useDashboardChannelConfig({
|
|||
setIsSavingGlobalDelivery,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedBotId || !selectedBot) {
|
||||
setGlobalDelivery({ sendProgress: false, sendToolHints: false });
|
||||
return;
|
||||
}
|
||||
setGlobalDelivery({
|
||||
sendProgress: Boolean(selectedBot.send_progress),
|
||||
sendToolHints: Boolean(selectedBot.send_tool_hints),
|
||||
});
|
||||
}, [selectedBot, selectedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
if (channelCreateMenuRef.current && !channelCreateMenuRef.current.contains(event.target as Node)) {
|
||||
|
|
@ -155,7 +172,7 @@ export function useDashboardChannelConfig({
|
|||
setNewChannelPanelOpen(false);
|
||||
setChannelCreateMenuOpen(false);
|
||||
resetNewChannelDraft();
|
||||
setGlobalDelivery({ sendProgress: false, sendToolHints: false });
|
||||
setGlobalDeliveryDraftByBot({});
|
||||
}, [resetNewChannelDraft]);
|
||||
|
||||
const channelConfigModalProps = {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../.
|
|||
import type { QuotedReply, StagedSubmissionDraft } from '../types';
|
||||
import type { DashboardChatNotifyOptions } from './dashboardChatShared';
|
||||
|
||||
interface ChatCommandDispatchLabels {
|
||||
export interface ChatCommandDispatchLabels {
|
||||
attachmentMessage: string;
|
||||
quoteOnlyMessage: string;
|
||||
backendDeliverFail: string;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, useRef, useState, type Dispatch, type KeyboardEvent, type SetStateAction } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type KeyboardEvent, type SetStateAction } from 'react';
|
||||
|
||||
import type { ChatMessage } from '../../../types/bot';
|
||||
import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText';
|
||||
import type { QuotedReply } from '../types';
|
||||
import { loadComposerDraft, persistComposerDraft } from '../utils';
|
||||
import type { DashboardChatNotifyOptions } from './dashboardChatShared';
|
||||
import { useDashboardChatCommandDispatch } from './useDashboardChatCommandDispatch';
|
||||
import { useDashboardChatStaging } from './useDashboardChatStaging';
|
||||
import { useDashboardChatCommandDispatch, type ChatCommandDispatchLabels } from './useDashboardChatCommandDispatch';
|
||||
import { useDashboardChatStaging, type StagedSubmissionLabels } from './useDashboardChatStaging';
|
||||
|
||||
const COMPOSER_MIN_ROWS = 3;
|
||||
const COMPOSER_MAX_HEIGHT_PX = 220;
|
||||
|
|
@ -25,7 +25,15 @@ interface UseDashboardChatComposerOptions {
|
|||
addBotMessage: (botId: string, message: Partial<ChatMessage> & Pick<ChatMessage, 'role' | 'text' | 'ts'>) => void;
|
||||
scrollConversationToBottom: (behavior?: ScrollBehavior) => void;
|
||||
notify: (message: string, options?: DashboardChatNotifyOptions) => void;
|
||||
t: any;
|
||||
t: DashboardChatComposerLabels;
|
||||
}
|
||||
|
||||
interface DashboardChatComposerLabels extends ChatCommandDispatchLabels, StagedSubmissionLabels {
|
||||
copyPromptDone: string;
|
||||
copyPromptFail: string;
|
||||
editPromptDone: string;
|
||||
copyReplyDone: string;
|
||||
copyReplyFail: string;
|
||||
}
|
||||
|
||||
export function useDashboardChatComposer({
|
||||
|
|
@ -44,16 +52,40 @@ export function useDashboardChatComposer({
|
|||
notify,
|
||||
t,
|
||||
}: UseDashboardChatComposerOptions) {
|
||||
const [command, setCommand] = useState('');
|
||||
const [composerDraftHydrated, setComposerDraftHydrated] = useState(false);
|
||||
const [quotedReply, setQuotedReply] = useState<QuotedReply | null>(null);
|
||||
const [commandByBot, setCommandByBot] = useState<Record<string, string>>({});
|
||||
const [quotedReplyByBot, setQuotedReplyByBot] = useState<Record<string, QuotedReply | null>>({});
|
||||
|
||||
const filePickerRef = useRef<HTMLInputElement | null>(null);
|
||||
const composerTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const stagedAutoSubmitAttemptByBotRef = useRef<Record<string, string>>({});
|
||||
|
||||
const persistedComposerDraft = useMemo(
|
||||
() => (selectedBotId ? loadComposerDraft(selectedBotId) : null),
|
||||
[selectedBotId],
|
||||
);
|
||||
|
||||
const command = selectedBotId ? (commandByBot[selectedBotId] ?? persistedComposerDraft?.command ?? '') : '';
|
||||
const quotedReply = selectedBotId ? (quotedReplyByBot[selectedBotId] ?? null) : null;
|
||||
const hasComposerDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply);
|
||||
|
||||
const setCommand = useCallback((value: SetStateAction<string>) => {
|
||||
if (!selectedBotId) return;
|
||||
setCommandByBot((prev) => {
|
||||
const currentValue = prev[selectedBotId] ?? persistedComposerDraft?.command ?? '';
|
||||
const nextValue = typeof value === 'function' ? value(currentValue) : value;
|
||||
return { ...prev, [selectedBotId]: nextValue };
|
||||
});
|
||||
}, [persistedComposerDraft?.command, selectedBotId]);
|
||||
|
||||
const setQuotedReply = useCallback((value: SetStateAction<QuotedReply | null>) => {
|
||||
if (!selectedBotId) return;
|
||||
setQuotedReplyByBot((prev) => {
|
||||
const currentValue = prev[selectedBotId] ?? null;
|
||||
const nextValue = typeof value === 'function' ? value(currentValue) : value;
|
||||
return { ...prev, [selectedBotId]: nextValue };
|
||||
});
|
||||
}, [selectedBotId]);
|
||||
|
||||
const {
|
||||
completeLeadingStagedSubmission,
|
||||
nextQueuedSubmission,
|
||||
|
|
@ -109,27 +141,9 @@ export function useDashboardChatComposer({
|
|||
: 'send';
|
||||
|
||||
useEffect(() => {
|
||||
setComposerDraftHydrated(false);
|
||||
if (!selectedBotId) {
|
||||
setCommand('');
|
||||
setPendingAttachments([]);
|
||||
setComposerDraftHydrated(true);
|
||||
return;
|
||||
}
|
||||
const draft = loadComposerDraft(selectedBotId);
|
||||
setCommand(draft?.command || '');
|
||||
setPendingAttachments(draft?.attachments || []);
|
||||
setComposerDraftHydrated(true);
|
||||
}, [selectedBotId, setPendingAttachments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedBotId || !composerDraftHydrated) return;
|
||||
if (!selectedBotId) return;
|
||||
persistComposerDraft(selectedBotId, command, pendingAttachments);
|
||||
}, [selectedBotId, composerDraftHydrated, command, pendingAttachments]);
|
||||
|
||||
useEffect(() => {
|
||||
setQuotedReply(null);
|
||||
}, [selectedBotId]);
|
||||
}, [selectedBotId, command, pendingAttachments]);
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = composerTextareaRef.current;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ interface UseDashboardChatStagingOptions {
|
|||
setQuotedReply: Dispatch<SetStateAction<QuotedReply | null>>;
|
||||
composerTextareaRef: RefObject<HTMLTextAreaElement | null>;
|
||||
notify: (message: string, options?: DashboardChatNotifyOptions) => void;
|
||||
t: any;
|
||||
t: StagedSubmissionLabels;
|
||||
}
|
||||
|
||||
export interface StagedSubmissionLabels {
|
||||
stagedSubmissionQueued: string;
|
||||
stagedSubmissionRestored: string;
|
||||
}
|
||||
|
||||
export function useDashboardChatStaging({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from 'react';
|
||||
|
||||
import { sortBotsByCreatedAtDesc } from '../../../shared/bot/sortBots';
|
||||
import type { BotState } from '../../../types/bot';
|
||||
|
|
@ -13,6 +13,10 @@ interface UseDashboardShellStateOptions {
|
|||
onCompactPanelTabChange?: (tab: CompactPanelTab) => void;
|
||||
}
|
||||
|
||||
function resolveStateAction<T>(next: SetStateAction<T>, prev: T): T {
|
||||
return typeof next === 'function' ? (next as (prevState: T) => T)(prev) : next;
|
||||
}
|
||||
|
||||
export function useDashboardShellState({
|
||||
activeBots,
|
||||
botListPageSize,
|
||||
|
|
@ -21,17 +25,17 @@ export function useDashboardShellState({
|
|||
forcedBotId,
|
||||
onCompactPanelTabChange,
|
||||
}: UseDashboardShellStateOptions) {
|
||||
const [selectedBotId, setSelectedBotId] = useState('');
|
||||
const [selectedBotId, setSelectedBotIdState] = useState('');
|
||||
const [runtimeViewMode, setRuntimeViewMode] = useState<RuntimeViewMode>('visual');
|
||||
const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false);
|
||||
const [botListMenuOpen, setBotListMenuOpen] = useState(false);
|
||||
const [runtimeMenuBotId, setRuntimeMenuBotId] = useState<string | null>(null);
|
||||
const [botListMenuOpen, setBotListMenuOpenState] = useState(false);
|
||||
const [topicDetailOpen, setTopicDetailOpen] = useState(false);
|
||||
const [compactPanelTabState, setCompactPanelTabState] = useState<CompactPanelTab>('chat');
|
||||
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
||||
const [botListQuery, setBotListQuery] = useState('');
|
||||
const [botListPage, setBotListPage] = useState(1);
|
||||
const [isCompactMobileState, setIsCompactMobileState] = useState(false);
|
||||
const [botListQuery, setBotListQueryState] = useState('');
|
||||
const [botListPageState, setBotListPageState] = useState(1);
|
||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||
const [controlCommandPanelOpen, setControlCommandPanelOpen] = useState(false);
|
||||
const [controlCommandPanelBotId, setControlCommandPanelBotId] = useState<string | null>(null);
|
||||
|
||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const botListMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -44,20 +48,37 @@ export function useDashboardShellState({
|
|||
|
||||
const hasForcedBot = Boolean(String(forcedBotId || '').trim());
|
||||
const compactListFirstMode = compactMode && !hasForcedBot;
|
||||
const isCompactListPage = compactListFirstMode && !selectedBotId;
|
||||
const showCompactBotPageClose = compactListFirstMode && Boolean(selectedBotId);
|
||||
const compactPanelTab = compactPanelTabProp ?? compactPanelTabState;
|
||||
const compactPanelTab = compactMode ? (compactPanelTabProp ?? compactPanelTabState) : 'chat';
|
||||
const setCompactPanelTab = useCallback(
|
||||
(next: CompactPanelTab | ((prev: CompactPanelTab) => CompactPanelTab)) => {
|
||||
const resolved = typeof next === 'function' ? next(compactPanelTab) : next;
|
||||
const resolved = resolveStateAction(next, compactPanelTabProp ?? compactPanelTabState);
|
||||
if (compactPanelTabProp === undefined) {
|
||||
setCompactPanelTabState(resolved);
|
||||
}
|
||||
onCompactPanelTabChange?.(resolved);
|
||||
},
|
||||
[compactPanelTab, compactPanelTabProp, onCompactPanelTabChange],
|
||||
[compactPanelTabProp, compactPanelTabState, onCompactPanelTabChange],
|
||||
);
|
||||
const showBotListPanel = !hasForcedBot && (!compactMode || isCompactListPage);
|
||||
const setBotListQuery = useCallback((next: SetStateAction<string>) => {
|
||||
setBotListQueryState((prev) => resolveStateAction(next, prev));
|
||||
setBotListPageState(1);
|
||||
}, []);
|
||||
|
||||
const setBotListPage = useCallback((next: SetStateAction<number>) => {
|
||||
setBotListPageState((prev) => {
|
||||
const resolved = Number(resolveStateAction(next, prev));
|
||||
return Number.isFinite(resolved) ? Math.max(1, Math.trunc(resolved)) : 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setSelectedBotId = useCallback((next: SetStateAction<string>) => {
|
||||
setSelectedBotIdState((prev) => String(resolveStateAction(next, prev) || '').trim());
|
||||
setControlCommandPanelBotId(null);
|
||||
setRuntimeMenuBotId(null);
|
||||
setBotListMenuOpenState(false);
|
||||
}, []);
|
||||
|
||||
const normalizedForcedBotId = String(forcedBotId || '').trim();
|
||||
const normalizedBotListQuery = botListQuery.trim().toLowerCase();
|
||||
const filteredBots = useMemo(() => {
|
||||
if (!normalizedBotListQuery) return bots;
|
||||
|
|
@ -67,66 +88,66 @@ export function useDashboardShellState({
|
|||
return id.includes(normalizedBotListQuery) || name.includes(normalizedBotListQuery);
|
||||
});
|
||||
}, [bots, normalizedBotListQuery]);
|
||||
const forcedBotMissing = Boolean(forcedBotId && bots.length > 0 && !activeBots[String(forcedBotId).trim()]);
|
||||
|
||||
useEffect(() => {
|
||||
setBotListPage(1);
|
||||
}, [normalizedBotListQuery]);
|
||||
const forcedBotMissing = Boolean(normalizedForcedBotId && bots.length > 0 && !activeBots[normalizedForcedBotId]);
|
||||
|
||||
const botListTotalPages = Math.max(1, Math.ceil(filteredBots.length / botListPageSize));
|
||||
const botListPage = Math.min(Math.max(1, botListPageState), botListTotalPages);
|
||||
const pagedBots = useMemo(() => {
|
||||
const page = Math.min(Math.max(1, botListPage), botListTotalPages);
|
||||
const start = (page - 1) * botListPageSize;
|
||||
return filteredBots.slice(start, start + botListPageSize);
|
||||
}, [botListPage, botListPageSize, botListTotalPages, filteredBots]);
|
||||
|
||||
const selectedBot = selectedBotId ? activeBots[selectedBotId] : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
setBotListPage((prev) => Math.min(Math.max(prev, 1), botListTotalPages));
|
||||
}, [botListTotalPages]);
|
||||
|
||||
useEffect(() => {
|
||||
const forced = String(forcedBotId || '').trim();
|
||||
if (forced) {
|
||||
if (activeBots[forced]) {
|
||||
if (selectedBotId !== forced) setSelectedBotId(forced);
|
||||
} else if (selectedBotId) {
|
||||
setSelectedBotId('');
|
||||
}
|
||||
return;
|
||||
const selectedBotIdResolved = useMemo(() => {
|
||||
if (normalizedForcedBotId) {
|
||||
return activeBots[normalizedForcedBotId] ? normalizedForcedBotId : '';
|
||||
}
|
||||
if (compactListFirstMode) {
|
||||
if (selectedBotId && !activeBots[selectedBotId]) {
|
||||
setSelectedBotId('');
|
||||
return selectedBotId && activeBots[selectedBotId] ? selectedBotId : '';
|
||||
}
|
||||
return;
|
||||
if (selectedBotId && activeBots[selectedBotId]) {
|
||||
return selectedBotId;
|
||||
}
|
||||
if (!selectedBotId && bots.length > 0) setSelectedBotId(bots[0].id);
|
||||
if (selectedBotId && !activeBots[selectedBotId] && bots.length > 0) setSelectedBotId(bots[0].id);
|
||||
}, [activeBots, bots, compactListFirstMode, forcedBotId, selectedBotId]);
|
||||
return bots[0]?.id || '';
|
||||
}, [activeBots, bots, compactListFirstMode, normalizedForcedBotId, selectedBotId]);
|
||||
|
||||
const isCompactListPage = compactListFirstMode && !selectedBotIdResolved;
|
||||
const showCompactBotPageClose = compactListFirstMode && Boolean(selectedBotIdResolved);
|
||||
const showBotListPanel = !hasForcedBot && (!compactMode || isCompactListPage);
|
||||
const selectedBot = selectedBotIdResolved ? activeBots[selectedBotIdResolved] : undefined;
|
||||
const runtimeMenuOpen = Boolean(selectedBotIdResolved && runtimeMenuBotId === selectedBotIdResolved);
|
||||
const controlCommandPanelOpen = Boolean(
|
||||
selectedBotIdResolved && controlCommandPanelBotId === selectedBotIdResolved,
|
||||
);
|
||||
|
||||
const setRuntimeMenuOpen = useCallback((next: SetStateAction<boolean>) => {
|
||||
setRuntimeMenuBotId((prev) => {
|
||||
const prevOpen = Boolean(selectedBotIdResolved && prev === selectedBotIdResolved);
|
||||
const resolved = resolveStateAction(next, prevOpen);
|
||||
return resolved && selectedBotIdResolved ? selectedBotIdResolved : null;
|
||||
});
|
||||
}, [selectedBotIdResolved]);
|
||||
|
||||
const setControlCommandPanelOpen = useCallback((next: SetStateAction<boolean>) => {
|
||||
setControlCommandPanelBotId((prev) => {
|
||||
const prevOpen = Boolean(selectedBotIdResolved && prev === selectedBotIdResolved);
|
||||
const resolved = resolveStateAction(next, prevOpen);
|
||||
return resolved && selectedBotIdResolved ? selectedBotIdResolved : null;
|
||||
});
|
||||
}, [selectedBotIdResolved]);
|
||||
|
||||
const setBotListMenuOpen = useCallback((next: SetStateAction<boolean>) => {
|
||||
setBotListMenuOpenState((prev) => resolveStateAction(next, prev));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setControlCommandPanelOpen(false);
|
||||
}, [selectedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
setRuntimeMenuOpen(false);
|
||||
setBotListMenuOpen(false);
|
||||
}, [selectedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compactMode) {
|
||||
setIsCompactMobile(false);
|
||||
setCompactPanelTab('chat');
|
||||
return;
|
||||
}
|
||||
if (!compactMode) return;
|
||||
const media = window.matchMedia('(max-width: 980px)');
|
||||
const apply = () => setIsCompactMobile(media.matches);
|
||||
const apply = () => setIsCompactMobileState(media.matches);
|
||||
apply();
|
||||
media.addEventListener('change', apply);
|
||||
return () => media.removeEventListener('change', apply);
|
||||
}, [compactMode, setCompactPanelTab]);
|
||||
}, [compactMode]);
|
||||
|
||||
return {
|
||||
botListMenuOpen,
|
||||
|
|
@ -143,20 +164,20 @@ export function useDashboardShellState({
|
|||
forcedBotMissing,
|
||||
hasForcedBot,
|
||||
isCompactListPage,
|
||||
isCompactMobile,
|
||||
isCompactMobile: compactMode && isCompactMobileState,
|
||||
normalizedBotListQuery,
|
||||
pagedBots,
|
||||
runtimeMenuOpen,
|
||||
runtimeMenuRef,
|
||||
runtimeViewMode,
|
||||
selectedBot,
|
||||
selectedBotId,
|
||||
selectedBotId: selectedBotIdResolved,
|
||||
setBotListMenuOpen,
|
||||
setBotListPage,
|
||||
setBotListQuery,
|
||||
setCompactPanelTab,
|
||||
setControlCommandPanelOpen,
|
||||
setIsCompactMobile,
|
||||
setIsCompactMobile: setIsCompactMobileState,
|
||||
setRuntimeMenuOpen,
|
||||
setRuntimeViewMode,
|
||||
setSelectedBotId,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, RefreshCw, Terminal } from 'lucide-react';
|
||||
|
||||
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
|
||||
|
|
@ -42,10 +42,7 @@ export function PlatformBotRuntimeSection({
|
|||
const dashboardT = pickLocale(isZh ? 'zh' : 'en', { 'zh-cn': dashboardZhCn, en: dashboardEn });
|
||||
const dockerLogsCardRef = useRef<HTMLDivElement | null>(null);
|
||||
const [workspaceCardHeightPx, setWorkspaceCardHeightPx] = useState<number | null>(null);
|
||||
const workspaceSearchInputName = useMemo(
|
||||
() => `platform-workspace-search-${Math.random().toString(36).slice(2, 10)}`,
|
||||
[],
|
||||
);
|
||||
const workspaceSearchInputName = `platform-workspace-search-${useId().replace(/:/g, '-')}`;
|
||||
const effectivePageSize = Math.max(1, Math.trunc(pageSize || 10));
|
||||
const dockerLogsTableHeightPx = DOCKER_LOG_TABLE_HEADER_HEIGHT + effectivePageSize * DOCKER_LOG_TABLE_ROW_HEIGHT;
|
||||
const workspaceCardStyle = useMemo(
|
||||
|
|
|
|||
|
|
@ -14,16 +14,38 @@ import {
|
|||
workspaceFileAction,
|
||||
} from './utils';
|
||||
import type { WorkspaceAttachmentPolicySnapshot, WorkspaceNotifyOptions } from './workspaceShared';
|
||||
import { useWorkspaceAttachments } from './useWorkspaceAttachments';
|
||||
import { useWorkspaceAttachments, type WorkspaceAttachmentLabels } from './useWorkspaceAttachments';
|
||||
import { useWorkspacePreview } from './useWorkspacePreview';
|
||||
|
||||
interface WorkspaceTreeLabels {
|
||||
workspaceLoadFail: string;
|
||||
}
|
||||
|
||||
interface ApiErrorDetail {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
function resolveApiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (axios.isAxiosError<ApiErrorDetail>(error)) {
|
||||
const detail = String(error.response?.data?.detail || '').trim();
|
||||
if (detail) return detail;
|
||||
const message = String(error.message || '').trim();
|
||||
if (message) return message;
|
||||
} else if (error instanceof Error) {
|
||||
const message = String(error.message || '').trim();
|
||||
if (message) return message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
interface UseBotWorkspaceOptions {
|
||||
selectedBotId: string;
|
||||
selectedBotDockerStatus?: string;
|
||||
workspaceDownloadExtensions: string[];
|
||||
refreshAttachmentPolicy: () => Promise<WorkspaceAttachmentPolicySnapshot>;
|
||||
restorePendingAttachments?: (botId: string) => string[];
|
||||
notify: (message: string, options?: WorkspaceNotifyOptions) => void;
|
||||
t: any;
|
||||
t: WorkspaceTreeLabels & WorkspaceAttachmentLabels & Record<string, unknown>;
|
||||
isZh: boolean;
|
||||
fileNotPreviewableLabel: string;
|
||||
}
|
||||
|
|
@ -33,6 +55,7 @@ export function useBotWorkspace({
|
|||
selectedBotDockerStatus,
|
||||
workspaceDownloadExtensions,
|
||||
refreshAttachmentPolicy,
|
||||
restorePendingAttachments,
|
||||
notify,
|
||||
t,
|
||||
isZh,
|
||||
|
|
@ -98,12 +121,12 @@ export function useBotWorkspace({
|
|||
setWorkspaceSearchEntries([]);
|
||||
setWorkspaceCurrentPath(res.data?.cwd || '');
|
||||
setWorkspaceParentPath(res.data?.parent ?? null);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
setWorkspaceEntries([]);
|
||||
setWorkspaceSearchEntries([]);
|
||||
setWorkspaceCurrentPath('');
|
||||
setWorkspaceParentPath(null);
|
||||
setWorkspaceError(error?.response?.data?.detail || t.workspaceLoadFail);
|
||||
setWorkspaceError(resolveApiErrorMessage(error, t.workspaceLoadFail));
|
||||
} finally {
|
||||
setWorkspaceLoading(false);
|
||||
}
|
||||
|
|
@ -174,6 +197,7 @@ export function useBotWorkspace({
|
|||
workspaceCurrentPath,
|
||||
loadWorkspaceTree,
|
||||
refreshAttachmentPolicy,
|
||||
restorePendingAttachments,
|
||||
notify,
|
||||
t,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useState, type ChangeEvent } from 'react';
|
||||
import { useCallback, useMemo, useState, type ChangeEvent, type SetStateAction } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
import { APP_ENDPOINTS } from '../../config/env';
|
||||
|
|
@ -6,13 +6,37 @@ import type { WorkspaceUploadResponse } from './types';
|
|||
import { isMediaUploadFile, normalizeAttachmentPaths } from './utils';
|
||||
import type { WorkspaceAttachmentPolicySnapshot, WorkspaceNotifyOptions } from './workspaceShared';
|
||||
|
||||
export interface WorkspaceAttachmentLabels {
|
||||
uploadTypeNotAllowed: (files: string, allowed: string) => string;
|
||||
uploadTooLarge: (files: string, limitMb: number) => string;
|
||||
uploadFail: string;
|
||||
}
|
||||
|
||||
interface ApiErrorDetail {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
function resolveApiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (axios.isAxiosError<ApiErrorDetail>(error)) {
|
||||
const detail = String(error.response?.data?.detail || '').trim();
|
||||
if (detail) return detail;
|
||||
const message = String(error.message || '').trim();
|
||||
if (message) return message;
|
||||
} else if (error instanceof Error) {
|
||||
const message = String(error.message || '').trim();
|
||||
if (message) return message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
interface UseWorkspaceAttachmentsOptions {
|
||||
selectedBotId: string;
|
||||
workspaceCurrentPath: string;
|
||||
loadWorkspaceTree: (botId: string, path?: string) => Promise<void>;
|
||||
refreshAttachmentPolicy: () => Promise<WorkspaceAttachmentPolicySnapshot>;
|
||||
restorePendingAttachments?: (botId: string) => string[];
|
||||
notify: (message: string, options?: WorkspaceNotifyOptions) => void;
|
||||
t: any;
|
||||
t: WorkspaceAttachmentLabels;
|
||||
}
|
||||
|
||||
export function useWorkspaceAttachments({
|
||||
|
|
@ -20,18 +44,45 @@ export function useWorkspaceAttachments({
|
|||
workspaceCurrentPath,
|
||||
loadWorkspaceTree,
|
||||
refreshAttachmentPolicy,
|
||||
restorePendingAttachments,
|
||||
notify,
|
||||
t,
|
||||
}: UseWorkspaceAttachmentsOptions) {
|
||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||
const [pendingAttachmentsByBot, setPendingAttachmentsByBot] = useState<Record<string, string[]>>({});
|
||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||
const [attachmentUploadPercent, setAttachmentUploadPercent] = useState<number | null>(null);
|
||||
|
||||
const restoredPendingAttachments = useMemo(
|
||||
() => (selectedBotId ? normalizeAttachmentPaths(restorePendingAttachments?.(selectedBotId) ?? []) : []),
|
||||
[restorePendingAttachments, selectedBotId],
|
||||
);
|
||||
|
||||
const pendingAttachments = useMemo(
|
||||
() => (selectedBotId ? pendingAttachmentsByBot[selectedBotId] ?? restoredPendingAttachments : []),
|
||||
[pendingAttachmentsByBot, restoredPendingAttachments, selectedBotId],
|
||||
);
|
||||
|
||||
const setPendingAttachments = useCallback((value: SetStateAction<string[]>) => {
|
||||
if (!selectedBotId) return;
|
||||
setPendingAttachmentsByBot((prev) => {
|
||||
const currentValue = prev[selectedBotId] ?? restoredPendingAttachments;
|
||||
const nextValue = typeof value === 'function' ? value(currentValue) : value;
|
||||
return {
|
||||
...prev,
|
||||
[selectedBotId]: normalizeAttachmentPaths(nextValue),
|
||||
};
|
||||
});
|
||||
}, [restoredPendingAttachments, selectedBotId]);
|
||||
|
||||
const resetPendingAttachments = useCallback(() => {
|
||||
setPendingAttachments([]);
|
||||
if (!selectedBotId) {
|
||||
setPendingAttachmentsByBot({});
|
||||
} else {
|
||||
setPendingAttachmentsByBot((prev) => ({ ...prev, [selectedBotId]: [] }));
|
||||
}
|
||||
setIsUploadingAttachments(false);
|
||||
setAttachmentUploadPercent(null);
|
||||
}, []);
|
||||
}, [selectedBotId]);
|
||||
|
||||
const onPickAttachments = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!selectedBotId || !event.target.files || event.target.files.length === 0) return;
|
||||
|
|
@ -121,15 +172,15 @@ export function useWorkspaceAttachments({
|
|||
setPendingAttachments((prev) => Array.from(new Set([...prev, ...uploadedPaths])));
|
||||
await loadWorkspaceTree(selectedBotId, workspaceCurrentPath);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.detail || t.uploadFail;
|
||||
} catch (error: unknown) {
|
||||
const msg = resolveApiErrorMessage(error, t.uploadFail);
|
||||
notify(msg, { tone: 'error' });
|
||||
} finally {
|
||||
setIsUploadingAttachments(false);
|
||||
setAttachmentUploadPercent(null);
|
||||
event.target.value = '';
|
||||
}
|
||||
}, [loadWorkspaceTree, notify, refreshAttachmentPolicy, selectedBotId, t, workspaceCurrentPath]);
|
||||
}, [loadWorkspaceTree, notify, refreshAttachmentPolicy, selectedBotId, setPendingAttachments, t, workspaceCurrentPath]);
|
||||
|
||||
return {
|
||||
attachmentUploadPercent,
|
||||
|
|
|
|||
Loading…
Reference in New Issue