import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import { ChevronLeft, ChevronRight, RefreshCw, Terminal } from 'lucide-react'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { dashboardEn } from '../../../i18n/dashboard.en'; import { dashboardZhCn } from '../../../i18n/dashboard.zh-cn'; import { pickLocale } from '../../../i18n'; import { WorkspaceEntriesList } from '../../../shared/workspace/WorkspaceEntriesList'; import { WorkspaceHoverCard } from '../../../shared/workspace/WorkspaceHoverCard'; import { WorkspacePreviewModal } from '../../../shared/workspace/WorkspacePreviewModal'; import '../../../shared/workspace/WorkspaceOverlay.css'; import { formatBytes, formatWorkspaceTime, isPreviewableWorkspaceFile } from '../../../shared/workspace/utils'; import { useBotWorkspace } from '../../../shared/workspace/useBotWorkspace'; import type { BotState } from '../../../types/bot'; import { usePlatformBotDockerLogs } from '../hooks/usePlatformBotDockerLogs'; import '../../dashboard/components/BotListPanel.css'; import '../../dashboard/components/RuntimePanel.css'; import '../../dashboard/components/DashboardShared.css'; import '../../../components/ui/SharedUi.css'; interface PlatformBotRuntimeSectionProps { compactSheet?: boolean; isZh: boolean; pageSize: number; selectedBotInfo?: BotState; workspaceDownloadExtensions: string[]; } const DOCKER_LOG_TABLE_HEADER_HEIGHT = 40; const DOCKER_LOG_TABLE_ROW_HEIGHT = 56; export function PlatformBotRuntimeSection({ compactSheet = false, isZh, pageSize, selectedBotInfo, workspaceDownloadExtensions, }: PlatformBotRuntimeSectionProps) { const { notify } = useLucentPrompt(); const dashboardT = pickLocale(isZh ? 'zh' : 'en', { 'zh-cn': dashboardZhCn, en: dashboardEn }); const dockerLogsCardRef = useRef(null); const [workspaceCardHeightPx, setWorkspaceCardHeightPx] = useState(null); 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( () => (!compactSheet && workspaceCardHeightPx ? { height: workspaceCardHeightPx } : undefined), [compactSheet, workspaceCardHeightPx], ); const refreshWorkspaceAttachmentPolicy = useCallback( async () => ({ uploadMaxMb: 0, allowedAttachmentExtensions: [], workspaceDownloadExtensions, }), [workspaceDownloadExtensions], ); const { closeWorkspacePreview, copyWorkspacePreviewPath, copyWorkspacePreviewUrl, filteredWorkspaceEntries, getWorkspaceDownloadHref, getWorkspaceRawHref, hideWorkspaceHoverCard, loadWorkspaceTree, openWorkspaceFilePreview, resetWorkspaceState, saveWorkspacePreviewMarkdown, setWorkspaceAutoRefresh, setWorkspacePreviewDraft, setWorkspacePreviewFullscreen, setWorkspacePreviewMode, setWorkspaceQuery, showWorkspaceHoverCard, workspaceAutoRefresh, workspaceCurrentPath, workspaceDownloadExtensionSet, workspaceError, workspaceFileLoading, workspaceHoverCard, workspaceLoading, workspaceParentPath, workspacePathDisplay, workspacePreview, workspacePreviewCanEdit, workspacePreviewDraft, workspacePreviewEditorEnabled, workspacePreviewFullscreen, workspacePreviewMarkdownComponents, workspacePreviewSaving, workspaceQuery, workspaceSearchLoading, } = useBotWorkspace({ selectedBotId: selectedBotInfo?.id || '', selectedBotDockerStatus: selectedBotInfo?.docker_status || '', workspaceDownloadExtensions, refreshAttachmentPolicy: refreshWorkspaceAttachmentPolicy, notify, t: dashboardT, isZh, fileNotPreviewableLabel: dashboardT.fileNotPreviewable, }); const normalizedWorkspaceQuery = workspaceQuery.trim().toLowerCase(); const hasVisibleWorkspaceEntries = filteredWorkspaceEntries.length > 0; const visibleWorkspaceFiles = filteredWorkspaceEntries.filter((entry) => entry.type === 'file'); const hasVisiblePreviewableFiles = visibleWorkspaceFiles.some((entry) => isPreviewableWorkspaceFile(entry, workspaceDownloadExtensionSet), ); const showWorkspaceEmptyState = !workspaceLoading && !workspaceSearchLoading && !workspaceError && !hasVisibleWorkspaceEntries; const showNoPreviewableFilesHint = Boolean( selectedBotInfo && !workspaceError && !normalizedWorkspaceQuery && visibleWorkspaceFiles.length > 0 && !hasVisiblePreviewableFiles, ); const { dockerLogsError, dockerLogsHasMore, dockerLogsLoading, dockerLogsPage, dockerLogTableRows, fetchDockerLogsPage, recentLogEntries, } = usePlatformBotDockerLogs({ effectivePageSize, isZh, selectedBotInfo, }); useEffect(() => { if (!selectedBotInfo?.id) { resetWorkspaceState(); return; } resetWorkspaceState(); void loadWorkspaceTree(selectedBotInfo.id, ''); // Re-run only when the selected bot changes; loadWorkspaceTree is recreated // by workspace policy updates and would otherwise cause an initialization loop. // eslint-disable-next-line react-hooks/exhaustive-deps }, [resetWorkspaceState, selectedBotInfo?.id]); useEffect(() => { if (compactSheet) { setWorkspaceCardHeightPx(null); return; } const cardEl = dockerLogsCardRef.current; if (!cardEl) return; const syncHeight = () => { const nextHeight = Math.round(cardEl.getBoundingClientRect().height); setWorkspaceCardHeightPx((current) => (current === nextHeight ? current : nextHeight)); }; syncHeight(); if (typeof ResizeObserver === 'undefined') { window.addEventListener('resize', syncHeight); return () => window.removeEventListener('resize', syncHeight); } const observer = new ResizeObserver(() => { syncHeight(); }); observer.observe(cardEl); window.addEventListener('resize', syncHeight); return () => { observer.disconnect(); window.removeEventListener('resize', syncHeight); }; }, [compactSheet, selectedBotInfo?.id]); return ( <>
{!compactSheet ?
{isZh ? 'Workspace' : 'Workspace'}
: null}
{dashboardT.workspaceHint}
{workspaceError ?
{workspaceError}
: null}
{workspacePathDisplay}
selectedBotInfo ? void loadWorkspaceTree(selectedBotInfo.id, workspaceCurrentPath) : undefined} tooltip={isZh ? '刷新工作区' : 'Refresh workspace'} aria-label={isZh ? '刷新工作区' : 'Refresh workspace'} >
setWorkspaceQuery('')} onSearchAction={() => setWorkspaceQuery(workspaceQuery.trim())} debounceMs={200} placeholder={dashboardT.workspaceSearchPlaceholder} ariaLabel={dashboardT.workspaceSearchPlaceholder} clearTitle={dashboardT.clearSearch} searchTitle={dashboardT.searchAction} name={workspaceSearchInputName} id={workspaceSearchInputName} disabled={!selectedBotInfo} />
{!selectedBotInfo ? (
{isZh ? '从左侧选择一个 Bot 查看工作区。' : 'Select a bot from the list to view its workspace.'}
) : workspaceLoading || workspaceSearchLoading ? (
{dashboardT.loadingDir}
) : ( <> {(workspaceParentPath !== null || hasVisibleWorkspaceEntries) ? ( ) : null} {showWorkspaceEmptyState ? (
{normalizedWorkspaceQuery ? dashboardT.workspaceSearchNoResult : dashboardT.emptyDir}
) : null} )}
{workspaceFileLoading ? dashboardT.openingPreview : dashboardT.workspaceHint}
{showNoPreviewableFilesHint ? (
{dashboardT.noPreviewFile}
) : null}
{!compactSheet ?
{isZh ? 'Docker Logs' : 'Docker Logs'}
: null}
{isZh ? '直接按页读取容器日志,按时间倒序展示;无日志时回退到最近运行事件' : 'Reading container logs page by page in reverse chronological order; falls back to runtime events if logs are unavailable'}
void fetchDockerLogsPage(dockerLogsPage, false)} tooltip={isZh ? '刷新 Docker Logs' : 'Refresh Docker Logs'} aria-label={isZh ? '刷新 Docker Logs' : 'Refresh Docker Logs'} >
{selectedBotInfo ? (
{dockerLogsError ?
{dockerLogsError}
: null}
{dockerLogTableRows.map((entry) => { const isPlaceholder = !entry.text; return ( ); })}
{isZh ? '序号' : 'No.'} {isZh ? '类型' : 'Level'} {isZh ? '内容' : 'Message'}
{entry.index || '\u00A0'} {entry.level ? ( {entry.level} ) : (   )} {entry.text || '\u00A0'}
{recentLogEntries.length === 0 ? (
{dockerLogsLoading ? (isZh ? '读取 Docker 日志中...' : 'Loading Docker logs...') : (isZh ? '暂无 Docker 日志或运行事件。' : 'No Docker logs or runtime events yet.')}
) : null}
) : (
{isZh ? '从左侧选择一个 Bot 查看日志。' : 'Select a bot from the list to view logs.'}
)} {selectedBotInfo ? (
{isZh ? `第 ${dockerLogsPage} 页${dockerLogsHasMore ? ' · 可继续加载更早日志' : ''}` : `Page ${dockerLogsPage}${dockerLogsHasMore ? ' · more older logs available' : ''}`}
void fetchDockerLogsPage(dockerLogsPage - 1, false)} tooltip={isZh ? '更新日志' : 'Newer logs'} aria-label={isZh ? '更新日志' : 'Newer logs'} > void fetchDockerLogsPage(dockerLogsPage + 1, false)} tooltip={isZh ? '更早日志' : 'Older logs'} aria-label={isZh ? '更早日志' : 'Older logs'} >
) : null}
setWorkspacePreviewFullscreen((value) => !value)} onCopyPreviewPath={copyWorkspacePreviewPath} onCopyPreviewUrl={copyWorkspacePreviewUrl} onPreviewDraftChange={setWorkspacePreviewDraft} onSavePreviewMarkdown={saveWorkspacePreviewMarkdown} onEnterEditMode={() => setWorkspacePreviewMode('edit')} onExitEditMode={() => { setWorkspacePreviewMode('preview'); setWorkspacePreviewDraft(workspacePreview?.content || ''); }} getWorkspaceDownloadHref={getWorkspaceDownloadHref} getWorkspaceRawHref={getWorkspaceRawHref} /> ); }