main
mula.liu 2026-03-03 15:44:39 +08:00
parent 413a7d6efb
commit 0ef036621c
4 changed files with 180 additions and 88 deletions

View File

@ -0,0 +1,22 @@
import type { ReactNode } from 'react';
import './lucent-tooltip.css';
interface LucentTooltipProps {
content: string;
children: ReactNode;
side?: 'top' | 'bottom';
}
export function LucentTooltip({ content, children, side = 'top' }: LucentTooltipProps) {
const text = String(content || '').trim();
if (!text) return <>{children}</>;
return (
<span className={`lucent-tooltip-wrap side-${side}`}>
{children}
<span className="lucent-tooltip-bubble" role="tooltip">
{text}
</span>
</span>
);
}

View File

@ -0,0 +1,39 @@
.lucent-tooltip-wrap {
position: relative;
display: inline-flex;
}
.lucent-tooltip-bubble {
position: absolute;
left: 50%;
transform: translateX(-50%);
border: 1px solid color-mix(in oklab, var(--line) 72%, var(--brand) 28%);
border-radius: 8px;
background: color-mix(in oklab, var(--panel) 88%, #000 12%);
color: var(--text);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
padding: 5px 8px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s ease;
z-index: 40;
}
.lucent-tooltip-wrap.side-top .lucent-tooltip-bubble {
bottom: calc(100% + 8px);
}
.lucent-tooltip-wrap.side-bottom .lucent-tooltip-bubble {
top: calc(100% + 8px);
}
.lucent-tooltip-wrap:hover .lucent-tooltip-bubble,
.lucent-tooltip-wrap:focus-within .lucent-tooltip-bubble {
opacity: 1;
visibility: visible;
}

View File

@ -38,16 +38,20 @@
padding: 10px 10px 10px 14px; padding: 10px 10px 10px 14px;
margin-bottom: 10px; margin-bottom: 10px;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s ease, transform 0.2s ease; transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
} }
.ops-bot-card:hover { .ops-bot-card:hover {
border-color: var(--brand); border-color: var(--brand);
transform: translateY(-1px);
} }
.ops-bot-card.is-active { .ops-bot-card.is-active {
border-color: var(--brand); border-color: var(--brand);
box-shadow: inset 0 0 0 1px var(--brand); box-shadow: 0 10px 24px color-mix(in oklab, var(--brand) 22%, transparent), inset 0 0 0 1px var(--brand);
background: color-mix(in oklab, var(--panel-soft) 76%, var(--brand-soft) 24%);
transform: translateY(-1px);
z-index: 2;
} }
.ops-bot-top { .ops-bot-top {
@ -73,7 +77,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 6px;
} }
.ops-bot-strip { .ops-bot-strip {
@ -95,15 +99,50 @@
} }
.ops-bot-icon-btn { .ops-bot-icon-btn {
width: 34px; width: 31px;
height: 34px; height: 31px;
padding: 0; padding: 0;
border-radius: 9px; border-radius: 8px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.ops-bot-icon-btn svg {
width: 13px;
height: 13px;
stroke-width: 2.2;
}
.ops-bot-actions .ops-bot-action-monitor {
background: color-mix(in oklab, var(--panel-soft) 75%, var(--brand-soft) 25%);
border-color: color-mix(in oklab, var(--brand) 44%, var(--line) 56%);
color: color-mix(in oklab, var(--text) 76%, white 24%);
}
.ops-bot-actions .ops-bot-action-start {
background: color-mix(in oklab, var(--ok) 24%, var(--panel-soft) 76%);
border-color: color-mix(in oklab, var(--ok) 52%, var(--line) 48%);
color: color-mix(in oklab, var(--text) 76%, white 24%);
}
.ops-bot-actions .ops-bot-action-stop {
background: color-mix(in oklab, #f5af48 30%, var(--panel-soft) 70%);
border-color: color-mix(in oklab, #f5af48 58%, var(--line) 42%);
color: #5e3b00;
}
.ops-bot-actions .ops-bot-action-delete {
background: color-mix(in oklab, var(--err) 20%, var(--panel-soft) 80%);
border-color: color-mix(in oklab, var(--err) 48%, var(--line) 52%);
color: color-mix(in oklab, var(--text) 72%, white 28%);
}
.ops-bot-actions .ops-bot-icon-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.ops-control-pending { .ops-control-pending {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@ -17,6 +17,7 @@ import { pickLocale } from '../../i18n';
import { dashboardZhCn } from '../../i18n/dashboard.zh-cn'; import { dashboardZhCn } from '../../i18n/dashboard.zh-cn';
import { dashboardEn } from '../../i18n/dashboard.en'; import { dashboardEn } from '../../i18n/dashboard.en';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
import { LucentTooltip } from '../../components/lucent/LucentTooltip';
interface BotDashboardModuleProps { interface BotDashboardModuleProps {
onOpenCreateWizard?: () => void; onOpenCreateWizard?: () => void;
@ -1779,7 +1780,11 @@ export function BotDashboardModule({
}, [workspaceAutoRefresh, selectedBotId, selectedBot?.docker_status, workspaceCurrentPath]); }, [workspaceAutoRefresh, selectedBotId, selectedBot?.docker_status, workspaceCurrentPath]);
const saveBot = async (mode: 'params' | 'agent' | 'base') => { const saveBot = async (mode: 'params' | 'agent' | 'base') => {
if (!selectedBot) return; const targetBotId = String(selectedBot?.id || selectedBotId || '').trim();
if (!targetBotId) {
notify(isZh ? '未选中 Bot无法保存。' : 'No bot selected.', { tone: 'warning' });
return;
}
setIsSaving(true); setIsSaving(true);
try { try {
const payload: Record<string, string | number> = {}; const payload: Record<string, string | number> = {};
@ -1834,7 +1839,7 @@ export function BotDashboardModule({
payload.identity_md = editForm.identity_md; payload.identity_md = editForm.identity_md;
} }
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}`, payload); await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${targetBotId}`, payload);
await refresh(); await refresh();
setShowBaseModal(false); setShowBaseModal(false);
setShowParamModal(false); setShowParamModal(false);
@ -2027,26 +2032,27 @@ export function BotDashboardModule({
</div> </div>
<div className="ops-bot-meta">{t.image}: <span className="mono">{bot.image_tag || '-'}</span></div> <div className="ops-bot-meta">{t.image}: <span className="mono">{bot.image_tag || '-'}</span></div>
<div className="ops-bot-actions"> <div className="ops-bot-actions">
<LucentTooltip content={isZh ? '资源监测' : 'Resource Monitor'}>
<button <button
className="btn btn-secondary btn-sm ops-bot-icon-btn" className="btn btn-sm ops-bot-icon-btn ops-bot-action-monitor"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
openResourceMonitor(bot.id); openResourceMonitor(bot.id);
}} }}
title={isZh ? '资源监测' : 'Resource Monitor'}
aria-label={isZh ? '资源监测' : 'Resource Monitor'} aria-label={isZh ? '资源监测' : 'Resource Monitor'}
> >
<Gauge size={14} /> <Gauge size={14} />
</button> </button>
</LucentTooltip>
{bot.docker_status === 'RUNNING' ? ( {bot.docker_status === 'RUNNING' ? (
<LucentTooltip content={t.stop}>
<button <button
className="btn btn-danger btn-sm icon-btn" className="btn btn-sm ops-bot-icon-btn ops-bot-action-stop"
disabled={isOperating} disabled={isOperating}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
void stopBot(bot.id, bot.docker_status); void stopBot(bot.id, bot.docker_status);
}} }}
title={t.stop}
aria-label={t.stop} aria-label={t.stop}
> >
{isStopping ? ( {isStopping ? (
@ -2059,15 +2065,16 @@ export function BotDashboardModule({
</span> </span>
) : <Square size={14} />} ) : <Square size={14} />}
</button> </button>
</LucentTooltip>
) : ( ) : (
<LucentTooltip content={t.start}>
<button <button
className="btn btn-success btn-sm ops-bot-icon-btn" className="btn btn-sm ops-bot-icon-btn ops-bot-action-start"
disabled={isOperating} disabled={isOperating}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
void startBot(bot.id, bot.docker_status); void startBot(bot.id, bot.docker_status);
}} }}
title={t.start}
aria-label={t.start} aria-label={t.start}
> >
{isStarting ? ( {isStarting ? (
@ -2080,18 +2087,20 @@ export function BotDashboardModule({
</span> </span>
) : <Power size={14} />} ) : <Power size={14} />}
</button> </button>
</LucentTooltip>
)} )}
<LucentTooltip content={t.delete}>
<button <button
className="btn btn-danger btn-sm ops-bot-icon-btn" className="btn btn-sm ops-bot-icon-btn ops-bot-action-delete"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
void removeBot(bot.id); void removeBot(bot.id);
}} }}
title={t.delete}
aria-label={t.delete} aria-label={t.delete}
> >
<Trash2 size={14} /> <Trash2 size={14} />
</button> </button>
</LucentTooltip>
</div> </div>
</div> </div>
); );
@ -2521,31 +2530,11 @@ export function BotDashboardModule({
<div className="stack"> <div className="stack">
<div className="card summary-grid"> <div className="card summary-grid">
<div>{isZh ? '容器状态' : 'Container'}: <strong className="mono">{resourceSnapshot.docker_status}</strong></div> <div>{isZh ? '容器状态' : 'Container'}: <strong className="mono">{resourceSnapshot.docker_status}</strong></div>
<div>{isZh ? '容器名' : 'Container Name'}: <span className="mono">{resourceSnapshot.bot_id ? `worker_${resourceSnapshot.bot_id}` : '-'}</span></div>
<div>{isZh ? '基础镜像' : 'Base Image'}: <span className="mono">{resourceBot?.image_tag || '-'}</span></div>
<div>Provider/Model: <span className="mono">{resourceBot?.llm_provider || '-'} / {resourceBot?.llm_model || '-'}</span></div>
<div>{isZh ? '采样时间' : 'Collected'}: <span className="mono">{resourceSnapshot.collected_at}</span></div> <div>{isZh ? '采样时间' : 'Collected'}: <span className="mono">{resourceSnapshot.collected_at}</span></div>
<div> <div>{isZh ? '策略说明' : 'Policy'}: <strong>{isZh ? '资源值 0 = 不限制' : 'Value 0 = Unlimited'}</strong></div>
{isZh ? 'CPU限制生效' : 'CPU limit'}:{' '}
<strong>
{Number(resourceSnapshot.configured.cpu_cores) === 0
? (isZh ? '不限' : 'UNLIMITED')
: (resourceSnapshot.enforcement.cpu_limited ? 'YES' : 'NO')}
</strong>
</div>
<div>
{isZh ? '内存限制生效' : 'Memory limit'}:{' '}
<strong>
{Number(resourceSnapshot.configured.memory_mb) === 0
? (isZh ? '不限' : 'UNLIMITED')
: (resourceSnapshot.enforcement.memory_limited ? 'YES' : 'NO')}
</strong>
</div>
<div>
{isZh ? '存储限制生效' : 'Storage limit'}:{' '}
<strong>
{Number(resourceSnapshot.configured.storage_gb) === 0
? (isZh ? '不限' : 'UNLIMITED')
: (resourceSnapshot.enforcement.storage_limited ? 'YES' : 'NO')}
</strong>
</div>
</div> </div>
<div className="grid-2" style={{ gridTemplateColumns: '1fr 1fr' }}> <div className="grid-2" style={{ gridTemplateColumns: '1fr 1fr' }}>
@ -2558,6 +2547,9 @@ export function BotDashboardModule({
<div className="card stack"> <div className="card stack">
<div className="section-mini-title">{isZh ? 'Docker 实际限制' : 'Docker Runtime Limits'}</div> <div className="section-mini-title">{isZh ? 'Docker 实际限制' : 'Docker Runtime Limits'}</div>
<div className="ops-runtime-row"><span>{isZh ? 'CPU限制生效' : 'CPU limit active'}</span><strong>{Number(resourceSnapshot.configured.cpu_cores) === 0 ? (isZh ? '不限' : 'Unlimited') : (resourceSnapshot.enforcement.cpu_limited ? 'YES' : 'NO')}</strong></div>
<div className="ops-runtime-row"><span>{isZh ? '内存限制生效' : 'Memory limit active'}</span><strong>{Number(resourceSnapshot.configured.memory_mb) === 0 ? (isZh ? '不限' : 'Unlimited') : (resourceSnapshot.enforcement.memory_limited ? 'YES' : 'NO')}</strong></div>
<div className="ops-runtime-row"><span>{isZh ? '存储限制生效' : 'Storage limit active'}</span><strong>{Number(resourceSnapshot.configured.storage_gb) === 0 ? (isZh ? '不限' : 'Unlimited') : (resourceSnapshot.enforcement.storage_limited ? 'YES' : 'NO')}</strong></div>
<div className="ops-runtime-row"><span>CPU</span><strong>{resourceSnapshot.runtime.limits.cpu_cores ? resourceSnapshot.runtime.limits.cpu_cores.toFixed(2) : (Number(resourceSnapshot.configured.cpu_cores) === 0 ? (isZh ? '不限' : 'Unlimited') : '-')}</strong></div> <div className="ops-runtime-row"><span>CPU</span><strong>{resourceSnapshot.runtime.limits.cpu_cores ? resourceSnapshot.runtime.limits.cpu_cores.toFixed(2) : (Number(resourceSnapshot.configured.cpu_cores) === 0 ? (isZh ? '不限' : 'Unlimited') : '-')}</strong></div>
<div className="ops-runtime-row"><span>{isZh ? '内存' : 'Memory'}</span><strong>{resourceSnapshot.runtime.limits.memory_bytes ? formatBytes(resourceSnapshot.runtime.limits.memory_bytes) : (Number(resourceSnapshot.configured.memory_mb) === 0 ? (isZh ? '不限' : 'Unlimited') : '-')}</strong></div> <div className="ops-runtime-row"><span>{isZh ? '内存' : 'Memory'}</span><strong>{resourceSnapshot.runtime.limits.memory_bytes ? formatBytes(resourceSnapshot.runtime.limits.memory_bytes) : (Number(resourceSnapshot.configured.memory_mb) === 0 ? (isZh ? '不限' : 'Unlimited') : '-')}</strong></div>
<div className="ops-runtime-row"><span>{isZh ? '存储' : 'Storage'}</span><strong>{resourceSnapshot.runtime.limits.storage_bytes ? formatBytes(resourceSnapshot.runtime.limits.storage_bytes) : (resourceSnapshot.runtime.limits.storage_opt_raw || (Number(resourceSnapshot.configured.storage_gb) === 0 ? (isZh ? '不限' : 'Unlimited') : '-'))}</strong></div> <div className="ops-runtime-row"><span>{isZh ? '存储' : 'Storage'}</span><strong>{resourceSnapshot.runtime.limits.storage_bytes ? formatBytes(resourceSnapshot.runtime.limits.storage_bytes) : (resourceSnapshot.runtime.limits.storage_opt_raw || (Number(resourceSnapshot.configured.storage_gb) === 0 ? (isZh ? '不限' : 'Unlimited') : '-'))}</strong></div>