v 0.1.3
parent
413a7d6efb
commit
0ef036621c
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -38,16 +38,20 @@
|
|||
padding: 10px 10px 10px 14px;
|
||||
margin-bottom: 10px;
|
||||
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 {
|
||||
border-color: var(--brand);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ops-bot-card.is-active {
|
||||
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 {
|
||||
|
|
@ -73,7 +77,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ops-bot-strip {
|
||||
|
|
@ -95,15 +99,50 @@
|
|||
}
|
||||
|
||||
.ops-bot-icon-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
padding: 0;
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: 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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { pickLocale } from '../../i18n';
|
|||
import { dashboardZhCn } from '../../i18n/dashboard.zh-cn';
|
||||
import { dashboardEn } from '../../i18n/dashboard.en';
|
||||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||
import { LucentTooltip } from '../../components/lucent/LucentTooltip';
|
||||
|
||||
interface BotDashboardModuleProps {
|
||||
onOpenCreateWizard?: () => void;
|
||||
|
|
@ -1779,7 +1780,11 @@ export function BotDashboardModule({
|
|||
}, [workspaceAutoRefresh, selectedBotId, selectedBot?.docker_status, workspaceCurrentPath]);
|
||||
|
||||
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);
|
||||
try {
|
||||
const payload: Record<string, string | number> = {};
|
||||
|
|
@ -1834,7 +1839,7 @@ export function BotDashboardModule({
|
|||
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();
|
||||
setShowBaseModal(false);
|
||||
setShowParamModal(false);
|
||||
|
|
@ -2027,26 +2032,27 @@ export function BotDashboardModule({
|
|||
</div>
|
||||
<div className="ops-bot-meta">{t.image}: <span className="mono">{bot.image_tag || '-'}</span></div>
|
||||
<div className="ops-bot-actions">
|
||||
<LucentTooltip content={isZh ? '资源监测' : 'Resource Monitor'}>
|
||||
<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) => {
|
||||
e.stopPropagation();
|
||||
openResourceMonitor(bot.id);
|
||||
}}
|
||||
title={isZh ? '资源监测' : 'Resource Monitor'}
|
||||
aria-label={isZh ? '资源监测' : 'Resource Monitor'}
|
||||
>
|
||||
<Gauge size={14} />
|
||||
</button>
|
||||
</LucentTooltip>
|
||||
{bot.docker_status === 'RUNNING' ? (
|
||||
<LucentTooltip content={t.stop}>
|
||||
<button
|
||||
className="btn btn-danger btn-sm icon-btn"
|
||||
className="btn btn-sm ops-bot-icon-btn ops-bot-action-stop"
|
||||
disabled={isOperating}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void stopBot(bot.id, bot.docker_status);
|
||||
}}
|
||||
title={t.stop}
|
||||
aria-label={t.stop}
|
||||
>
|
||||
{isStopping ? (
|
||||
|
|
@ -2059,15 +2065,16 @@ export function BotDashboardModule({
|
|||
</span>
|
||||
) : <Square size={14} />}
|
||||
</button>
|
||||
</LucentTooltip>
|
||||
) : (
|
||||
<LucentTooltip content={t.start}>
|
||||
<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}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void startBot(bot.id, bot.docker_status);
|
||||
}}
|
||||
title={t.start}
|
||||
aria-label={t.start}
|
||||
>
|
||||
{isStarting ? (
|
||||
|
|
@ -2080,18 +2087,20 @@ export function BotDashboardModule({
|
|||
</span>
|
||||
) : <Power size={14} />}
|
||||
</button>
|
||||
</LucentTooltip>
|
||||
)}
|
||||
<LucentTooltip content={t.delete}>
|
||||
<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) => {
|
||||
e.stopPropagation();
|
||||
void removeBot(bot.id);
|
||||
}}
|
||||
title={t.delete}
|
||||
aria-label={t.delete}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</LucentTooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -2521,31 +2530,11 @@ export function BotDashboardModule({
|
|||
<div className="stack">
|
||||
<div className="card summary-grid">
|
||||
<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 ? '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>{isZh ? '策略说明' : 'Policy'}: <strong>{isZh ? '资源值 0 = 不限制' : 'Value 0 = Unlimited'}</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="grid-2" style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
|
|
@ -2558,6 +2547,9 @@ export function BotDashboardModule({
|
|||
|
||||
<div className="card stack">
|
||||
<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>{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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue