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