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;
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue