327 lines
14 KiB
TypeScript
327 lines
14 KiB
TypeScript
|
|
import { Boxes, Check, Clock3, EllipsisVertical, FileText, Hammer, MessageSquareText, RefreshCw, RotateCcw, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, Waypoints } from 'lucide-react';
|
||
|
|
import type { RefObject } from 'react';
|
||
|
|
|
||
|
|
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
|
||
|
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||
|
|
import type { BotState } from '../../../types/bot';
|
||
|
|
import type { WorkspaceNode } from '../types';
|
||
|
|
import { WorkspaceEntriesList } from './WorkspaceEntriesList';
|
||
|
|
import './DashboardMenus.css';
|
||
|
|
import './RuntimePanel.css';
|
||
|
|
|
||
|
|
interface RuntimePanelLabels {
|
||
|
|
agent: string;
|
||
|
|
autoRefresh: string;
|
||
|
|
base: string;
|
||
|
|
channels: string;
|
||
|
|
clearHistory: string;
|
||
|
|
cronViewer: string;
|
||
|
|
download: string;
|
||
|
|
emptyDir: string;
|
||
|
|
envParams: string;
|
||
|
|
exportHistory: string;
|
||
|
|
fileNotPreviewable: string;
|
||
|
|
folder: string;
|
||
|
|
goUp: string;
|
||
|
|
goUpTitle: string;
|
||
|
|
loadingDir: string;
|
||
|
|
mcp: string;
|
||
|
|
more: string;
|
||
|
|
noPreviewFile: string;
|
||
|
|
openingPreview: string;
|
||
|
|
openFolderTitle: string;
|
||
|
|
params: string;
|
||
|
|
previewTitle: string;
|
||
|
|
refreshHint: string;
|
||
|
|
restart: string;
|
||
|
|
runtime: string;
|
||
|
|
searchAction: string;
|
||
|
|
skills: string;
|
||
|
|
topic: string;
|
||
|
|
workspaceHint: string;
|
||
|
|
workspaceOutputs: string;
|
||
|
|
workspaceSearchNoResult: string;
|
||
|
|
workspaceSearchPlaceholder: string;
|
||
|
|
clearSearch: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface RuntimePanelProps {
|
||
|
|
selectedBot?: BotState;
|
||
|
|
selectedBotEnabled: boolean;
|
||
|
|
operatingBotId: string | null;
|
||
|
|
runtimeMenuOpen: boolean;
|
||
|
|
runtimeMenuRef: RefObject<HTMLDivElement | null>;
|
||
|
|
displayState: string;
|
||
|
|
workspaceError: string;
|
||
|
|
workspacePathDisplay: string;
|
||
|
|
workspaceLoading: boolean;
|
||
|
|
workspaceQuery: string;
|
||
|
|
workspaceSearchInputName: string;
|
||
|
|
workspaceSearchLoading: boolean;
|
||
|
|
filteredWorkspaceEntries: WorkspaceNode[];
|
||
|
|
workspaceParentPath: string | null;
|
||
|
|
workspaceFileLoading: boolean;
|
||
|
|
workspaceDownloadExtensionSet: ReadonlySet<string>;
|
||
|
|
workspaceAutoRefresh: boolean;
|
||
|
|
hasPreviewFiles: boolean;
|
||
|
|
isCompactHidden: boolean;
|
||
|
|
showCompactSurface: boolean;
|
||
|
|
emptyStateText: string;
|
||
|
|
labels: RuntimePanelLabels;
|
||
|
|
onRestartBot: (botId: string, dockerStatus: string) => Promise<void> | void;
|
||
|
|
onToggleRuntimeMenu: () => void;
|
||
|
|
onOpenBaseConfig: () => Promise<void> | void;
|
||
|
|
onOpenParamConfig: () => Promise<void> | void;
|
||
|
|
onOpenChannelConfig: () => Promise<void> | void;
|
||
|
|
onOpenTopicConfig: () => Promise<void> | void;
|
||
|
|
onOpenEnvParams: () => Promise<void> | void;
|
||
|
|
onOpenSkills: () => Promise<void> | void;
|
||
|
|
onOpenMcpConfig: () => Promise<void> | void;
|
||
|
|
onOpenCronJobs: () => Promise<void> | void;
|
||
|
|
onOpenAgentFiles: () => Promise<void> | void;
|
||
|
|
onExportHistory: () => void;
|
||
|
|
onClearHistory: () => Promise<void> | void;
|
||
|
|
onRefreshWorkspace: () => Promise<void> | void;
|
||
|
|
onWorkspaceQueryChange: (value: string) => void;
|
||
|
|
onWorkspaceQueryClear: () => void;
|
||
|
|
onWorkspaceQuerySearch: () => void;
|
||
|
|
onToggleWorkspaceAutoRefresh: () => void;
|
||
|
|
onLoadWorkspaceTree: (botId: string, path?: string) => Promise<void> | void;
|
||
|
|
onOpenWorkspaceFilePreview: (path: string) => Promise<void> | void;
|
||
|
|
onShowWorkspaceHoverCard: (node: WorkspaceNode, anchor: HTMLElement) => void;
|
||
|
|
onHideWorkspaceHoverCard: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function RuntimePanel({
|
||
|
|
selectedBot,
|
||
|
|
selectedBotEnabled,
|
||
|
|
operatingBotId,
|
||
|
|
runtimeMenuOpen,
|
||
|
|
runtimeMenuRef,
|
||
|
|
displayState,
|
||
|
|
workspaceError,
|
||
|
|
workspacePathDisplay,
|
||
|
|
workspaceLoading,
|
||
|
|
workspaceQuery,
|
||
|
|
workspaceSearchInputName,
|
||
|
|
workspaceSearchLoading,
|
||
|
|
filteredWorkspaceEntries,
|
||
|
|
workspaceParentPath,
|
||
|
|
workspaceFileLoading,
|
||
|
|
workspaceDownloadExtensionSet,
|
||
|
|
workspaceAutoRefresh,
|
||
|
|
hasPreviewFiles,
|
||
|
|
isCompactHidden,
|
||
|
|
showCompactSurface,
|
||
|
|
emptyStateText,
|
||
|
|
labels,
|
||
|
|
onRestartBot,
|
||
|
|
onToggleRuntimeMenu,
|
||
|
|
onOpenBaseConfig,
|
||
|
|
onOpenParamConfig,
|
||
|
|
onOpenChannelConfig,
|
||
|
|
onOpenTopicConfig,
|
||
|
|
onOpenEnvParams,
|
||
|
|
onOpenSkills,
|
||
|
|
onOpenMcpConfig,
|
||
|
|
onOpenCronJobs,
|
||
|
|
onOpenAgentFiles,
|
||
|
|
onExportHistory,
|
||
|
|
onClearHistory,
|
||
|
|
onRefreshWorkspace,
|
||
|
|
onWorkspaceQueryChange,
|
||
|
|
onWorkspaceQueryClear,
|
||
|
|
onWorkspaceQuerySearch,
|
||
|
|
onToggleWorkspaceAutoRefresh,
|
||
|
|
onLoadWorkspaceTree,
|
||
|
|
onOpenWorkspaceFilePreview,
|
||
|
|
onShowWorkspaceHoverCard,
|
||
|
|
onHideWorkspaceHoverCard,
|
||
|
|
}: RuntimePanelProps) {
|
||
|
|
const normalizedWorkspaceQuery = workspaceQuery.trim().toLowerCase();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section className={`panel stack ops-runtime-panel ${isCompactHidden ? 'ops-compact-hidden' : ''} ${showCompactSurface ? 'ops-compact-bot-surface' : ''}`}>
|
||
|
|
{selectedBot ? (
|
||
|
|
<div className="ops-runtime-shell">
|
||
|
|
<div className="row-between ops-runtime-head">
|
||
|
|
<h2 style={{ fontSize: 18 }}>{labels.runtime}</h2>
|
||
|
|
<div className="ops-panel-tools" ref={runtimeMenuRef}>
|
||
|
|
<LucentIconButton
|
||
|
|
className="btn btn-secondary btn-sm icon-btn"
|
||
|
|
onClick={() => void onRestartBot(selectedBot.id, selectedBot.docker_status)}
|
||
|
|
disabled={operatingBotId === selectedBot.id || !selectedBotEnabled}
|
||
|
|
tooltip={labels.restart}
|
||
|
|
aria-label={labels.restart}
|
||
|
|
>
|
||
|
|
<RotateCcw size={14} className={operatingBotId === selectedBot.id ? 'animate-spin' : ''} />
|
||
|
|
</LucentIconButton>
|
||
|
|
<LucentIconButton
|
||
|
|
className="btn btn-secondary btn-sm icon-btn"
|
||
|
|
onClick={onToggleRuntimeMenu}
|
||
|
|
disabled={!selectedBotEnabled}
|
||
|
|
tooltip={labels.more}
|
||
|
|
aria-label={labels.more}
|
||
|
|
aria-haspopup="menu"
|
||
|
|
aria-expanded={runtimeMenuOpen}
|
||
|
|
>
|
||
|
|
<EllipsisVertical size={14} />
|
||
|
|
</LucentIconButton>
|
||
|
|
{runtimeMenuOpen ? (
|
||
|
|
<div className="ops-more-menu" role="menu" aria-label={labels.more}>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenBaseConfig()}>
|
||
|
|
<Settings2 size={14} />
|
||
|
|
<span>{labels.base}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenParamConfig()}>
|
||
|
|
<SlidersHorizontal size={14} />
|
||
|
|
<span>{labels.params}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenChannelConfig()}>
|
||
|
|
<Waypoints size={14} />
|
||
|
|
<span>{labels.channels}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenTopicConfig()}>
|
||
|
|
<MessageSquareText size={14} />
|
||
|
|
<span>{labels.topic}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenEnvParams()}>
|
||
|
|
<Settings2 size={14} />
|
||
|
|
<span>{labels.envParams}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenSkills()}>
|
||
|
|
<Hammer size={14} />
|
||
|
|
<span>{labels.skills}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenMcpConfig()}>
|
||
|
|
<Boxes size={14} />
|
||
|
|
<span>{labels.mcp}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenCronJobs()}>
|
||
|
|
<Clock3 size={14} />
|
||
|
|
<span>{labels.cronViewer}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={() => void onOpenAgentFiles()}>
|
||
|
|
<FileText size={14} />
|
||
|
|
<span>{labels.agent}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item" role="menuitem" onClick={onExportHistory}>
|
||
|
|
<Save size={14} />
|
||
|
|
<span>{labels.exportHistory}</span>
|
||
|
|
</button>
|
||
|
|
<button className="ops-more-item danger" role="menuitem" onClick={() => void onClearHistory()}>
|
||
|
|
<Trash2 size={14} />
|
||
|
|
<span>{labels.clearHistory}</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="ops-runtime-scroll">
|
||
|
|
<div className="card ops-runtime-card ops-runtime-state-card is-visual">
|
||
|
|
<div className={`ops-state-stage ops-state-${String(displayState).toLowerCase()}`}>
|
||
|
|
<div className="ops-state-model mono">{selectedBot.llm_model || '-'}</div>
|
||
|
|
<div className="ops-state-face" aria-hidden="true">
|
||
|
|
<span className="ops-state-eye left" />
|
||
|
|
<span className="ops-state-eye right" />
|
||
|
|
</div>
|
||
|
|
<div className="ops-state-caption mono">{String(displayState || 'IDLE').toUpperCase()}</div>
|
||
|
|
{displayState === 'TOOL_CALL' ? <Hammer size={18} className="ops-state-float state-tool" /> : null}
|
||
|
|
{displayState === 'SUCCESS' ? <Check size={18} className="ops-state-float state-success" /> : null}
|
||
|
|
{displayState === 'ERROR' ? <TriangleAlert size={18} className="ops-state-float state-error" /> : null}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="card ops-runtime-card">
|
||
|
|
<div className="section-mini-title">{labels.workspaceOutputs}</div>
|
||
|
|
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
||
|
|
<div className="workspace-toolbar">
|
||
|
|
<div className="workspace-path-wrap">
|
||
|
|
<div className="workspace-path mono" title={workspacePathDisplay}>
|
||
|
|
{workspacePathDisplay}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="workspace-toolbar-actions">
|
||
|
|
<LucentIconButton
|
||
|
|
className="workspace-refresh-icon-btn"
|
||
|
|
disabled={workspaceLoading}
|
||
|
|
onClick={() => void onRefreshWorkspace()}
|
||
|
|
tooltip={labels.refreshHint}
|
||
|
|
aria-label={labels.refreshHint}
|
||
|
|
>
|
||
|
|
<RefreshCw size={14} className={workspaceLoading ? 'animate-spin' : ''} />
|
||
|
|
</LucentIconButton>
|
||
|
|
<label className="workspace-auto-switch" title={labels.autoRefresh}>
|
||
|
|
<span className="workspace-auto-switch-label">{labels.autoRefresh}</span>
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={workspaceAutoRefresh}
|
||
|
|
onChange={onToggleWorkspaceAutoRefresh}
|
||
|
|
aria-label={labels.autoRefresh}
|
||
|
|
/>
|
||
|
|
<span className="workspace-auto-switch-track" />
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="workspace-search-toolbar">
|
||
|
|
<ProtectedSearchInput
|
||
|
|
value={workspaceQuery}
|
||
|
|
onChange={onWorkspaceQueryChange}
|
||
|
|
onClear={onWorkspaceQueryClear}
|
||
|
|
onSearchAction={onWorkspaceQuerySearch}
|
||
|
|
debounceMs={200}
|
||
|
|
placeholder={labels.workspaceSearchPlaceholder}
|
||
|
|
ariaLabel={labels.workspaceSearchPlaceholder}
|
||
|
|
clearTitle={labels.clearSearch}
|
||
|
|
searchTitle={labels.searchAction}
|
||
|
|
name={workspaceSearchInputName}
|
||
|
|
id={workspaceSearchInputName}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="workspace-panel">
|
||
|
|
<div className="workspace-list">
|
||
|
|
{workspaceLoading || workspaceSearchLoading ? (
|
||
|
|
<div className="ops-empty-inline">{labels.loadingDir}</div>
|
||
|
|
) : filteredWorkspaceEntries.length === 0 && workspaceParentPath === null ? (
|
||
|
|
<div className="ops-empty-inline">{normalizedWorkspaceQuery ? labels.workspaceSearchNoResult : labels.emptyDir}</div>
|
||
|
|
) : (
|
||
|
|
<WorkspaceEntriesList
|
||
|
|
nodes={filteredWorkspaceEntries}
|
||
|
|
workspaceParentPath={workspaceParentPath}
|
||
|
|
selectedBotId={selectedBot.id}
|
||
|
|
workspaceFileLoading={workspaceFileLoading}
|
||
|
|
workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
|
||
|
|
labels={{
|
||
|
|
download: labels.download,
|
||
|
|
fileNotPreviewable: labels.fileNotPreviewable,
|
||
|
|
folder: labels.folder,
|
||
|
|
goUp: labels.goUp,
|
||
|
|
goUpTitle: labels.goUpTitle,
|
||
|
|
openFolderTitle: labels.openFolderTitle,
|
||
|
|
previewTitle: labels.previewTitle,
|
||
|
|
}}
|
||
|
|
onLoadWorkspaceTree={onLoadWorkspaceTree}
|
||
|
|
onOpenWorkspaceFilePreview={onOpenWorkspaceFilePreview}
|
||
|
|
onShowWorkspaceHoverCard={onShowWorkspaceHoverCard}
|
||
|
|
onHideWorkspaceHoverCard={onHideWorkspaceHoverCard}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="workspace-hint">
|
||
|
|
{workspaceFileLoading ? labels.openingPreview : labels.workspaceHint}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{!hasPreviewFiles ? (
|
||
|
|
<div className="ops-empty-inline">{labels.noPreviewFile}</div>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div style={{ color: 'var(--muted)' }}>{emptyStateText}</div>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|