main
mula.liu 2026-03-03 16:12:27 +08:00
parent 0ef036621c
commit 9ddeedf0b6
9 changed files with 300 additions and 239 deletions

View File

@ -10,6 +10,8 @@ import { BotDashboardModule } from './modules/dashboard/BotDashboardModule';
import { pickLocale } from './i18n';
import { appZhCn } from './i18n/app.zh-cn';
import { appEn } from './i18n/app.en';
import { LucentIconButton } from './components/lucent/LucentIconButton';
import { LucentTooltip } from './components/lucent/LucentTooltip';
import './App.css';
function App() {
@ -53,41 +55,45 @@ function App() {
<div className="global-switches">
<div className="switch-compact">
<button
className={`switch-btn ${theme === 'dark' ? 'active' : ''}`}
onClick={() => setTheme('dark')}
title={t.dark}
aria-label={t.dark}
>
<MoonStar size={14} />
</button>
<button
className={`switch-btn ${theme === 'light' ? 'active' : ''}`}
onClick={() => setTheme('light')}
title={t.light}
aria-label={t.light}
>
<SunMedium size={14} />
</button>
<LucentTooltip content={t.dark}>
<button
className={`switch-btn ${theme === 'dark' ? 'active' : ''}`}
onClick={() => setTheme('dark')}
aria-label={t.dark}
>
<MoonStar size={14} />
</button>
</LucentTooltip>
<LucentTooltip content={t.light}>
<button
className={`switch-btn ${theme === 'light' ? 'active' : ''}`}
onClick={() => setTheme('light')}
aria-label={t.light}
>
<SunMedium size={14} />
</button>
</LucentTooltip>
</div>
<div className="switch-compact">
<button
className={`switch-btn switch-btn-lang ${locale === 'zh' ? 'active' : ''}`}
onClick={() => setLocale('zh')}
title={t.zh}
aria-label={t.zh}
>
<span>ZH</span>
</button>
<button
className={`switch-btn switch-btn-lang ${locale === 'en' ? 'active' : ''}`}
onClick={() => setLocale('en')}
title={t.en}
aria-label={t.en}
>
<span>EN</span>
</button>
<LucentTooltip content={t.zh}>
<button
className={`switch-btn switch-btn-lang ${locale === 'zh' ? 'active' : ''}`}
onClick={() => setLocale('zh')}
aria-label={t.zh}
>
<span>ZH</span>
</button>
</LucentTooltip>
<LucentTooltip content={t.en}>
<button
className={`switch-btn switch-btn-lang ${locale === 'en' ? 'active' : ''}`}
onClick={() => setLocale('en')}
aria-label={t.en}
>
<span>EN</span>
</button>
</LucentTooltip>
</div>
</div>
</div>
@ -111,9 +117,9 @@ function App() {
<h3>{t.nav.images.title}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowImageFactory(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowImageFactory(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="app-modal-body">
@ -131,9 +137,9 @@ function App() {
<h3>{t.nav.onboarding.title}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowCreateWizard(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowCreateWizard(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="app-modal-body">

View File

@ -0,0 +1,29 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { LucentTooltip } from './LucentTooltip';
interface LucentIconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
tooltip?: string;
tooltipSide?: 'top' | 'bottom';
children: ReactNode;
}
export function LucentIconButton({
tooltip,
tooltipSide = 'top',
children,
'aria-label': ariaLabel,
...buttonProps
}: LucentIconButtonProps) {
const tipText = String(tooltip || ariaLabel || '').trim();
return (
<LucentTooltip content={tipText} side={tooltipSide}>
<button
{...buttonProps}
aria-label={String(ariaLabel || tipText || '').trim() || undefined}
>
{children}
</button>
</LucentTooltip>
);
}

View File

@ -1,6 +1,7 @@
import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react';
import { AlertCircle, AlertTriangle, CheckCircle2, Info, X } from 'lucide-react';
import { useAppStore } from '../../store/appStore';
import { LucentIconButton } from './LucentIconButton';
import './lucent-prompt.css';
type PromptTone = 'info' | 'success' | 'warning' | 'error';
@ -133,14 +134,14 @@ export function LucentPromptProvider({ children }: { children: ReactNode }) {
<div className="lucent-confirm-title">
{confirmState.title || (locale === 'zh' ? '请确认操作' : 'Please Confirm')}
</div>
<button
<LucentIconButton
className="lucent-confirm-close"
onClick={() => closeConfirm(false)}
aria-label={locale === 'zh' ? '关闭' : 'Close'}
title={locale === 'zh' ? '关闭' : 'Close'}
tooltip={locale === 'zh' ? '关闭' : 'Close'}
>
<X size={14} />
</button>
</LucentIconButton>
</div>
<div className="lucent-confirm-message">{confirmState.message}</div>
<div className="lucent-confirm-actions">
@ -165,4 +166,3 @@ export function useLucentPrompt() {
}
return ctx;
}

View File

@ -21,19 +21,40 @@
visibility: hidden;
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s ease;
z-index: 40;
box-shadow: 0 8px 18px rgba(6, 12, 24, 0.24);
}
.lucent-tooltip-bubble::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 7px;
height: 7px;
border-right: 1px solid color-mix(in oklab, var(--line) 72%, var(--brand) 28%);
border-bottom: 1px solid color-mix(in oklab, var(--line) 72%, var(--brand) 28%);
background: color-mix(in oklab, var(--panel) 88%, #000 12%);
}
.lucent-tooltip-wrap.side-top .lucent-tooltip-bubble {
bottom: calc(100% + 8px);
}
.lucent-tooltip-wrap.side-top .lucent-tooltip-bubble::after {
bottom: -5px;
}
.lucent-tooltip-wrap.side-bottom .lucent-tooltip-bubble {
top: calc(100% + 8px);
}
.lucent-tooltip-wrap.side-bottom .lucent-tooltip-bubble::after {
top: -5px;
transform: translateX(-50%) rotate(225deg);
}
.lucent-tooltip-wrap:hover .lucent-tooltip-bubble,
.lucent-tooltip-wrap:focus-within .lucent-tooltip-bubble {
opacity: 1;
visibility: visible;
}

View File

@ -99,19 +99,19 @@
}
.ops-bot-icon-btn {
width: 31px;
height: 31px;
width: 36px;
height: 36px;
padding: 0;
border-radius: 8px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.ops-bot-icon-btn svg {
width: 13px;
height: 13px;
stroke-width: 2.2;
width: 17px;
height: 17px;
stroke-width: 2.1;
}
.ops-bot-actions .ops-bot-action-monitor {

View File

@ -17,7 +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';
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
interface BotDashboardModuleProps {
onOpenCreateWizard?: () => void;
@ -905,7 +905,7 @@ export function BotDashboardModule({
<div className="ops-chat-meta-right">
<span className="mono">{formatClock(item.ts)}</span>
{collapsible ? (
<button
<LucentIconButton
className="ops-chat-expand-icon-btn"
onClick={() =>
setExpandedProgressByKey((prev) => ({
@ -913,11 +913,11 @@ export function BotDashboardModule({
[itemKey]: !prev[itemKey],
}))
}
title={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
tooltip={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
aria-label={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
>
{expanded ? '×' : '…'}
</button>
</LucentIconButton>
) : null}
</div>
</div>
@ -1994,22 +1994,22 @@ export function BotDashboardModule({
<div className="row-between">
<h2 style={{ fontSize: 18 }}>{t.titleBots}</h2>
<div className="ops-list-actions">
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={onOpenImageFactory}
title={t.manageImages}
tooltip={t.manageImages}
aria-label={t.manageImages}
>
<Boxes size={14} />
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-primary btn-sm icon-btn"
onClick={onOpenCreateWizard}
title={t.newBot}
tooltip={t.newBot}
aria-label={t.newBot}
>
<Plus size={14} />
</button>
</LucentIconButton>
</div>
</div>
@ -2032,75 +2032,71 @@ 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-sm ops-bot-icon-btn ops-bot-action-monitor"
onClick={(e) => {
e.stopPropagation();
openResourceMonitor(bot.id);
}}
aria-label={isZh ? '资源监测' : 'Resource Monitor'}
>
<Gauge size={14} />
</button>
</LucentTooltip>
<LucentIconButton
className="btn btn-sm ops-bot-icon-btn ops-bot-action-monitor"
onClick={(e) => {
e.stopPropagation();
openResourceMonitor(bot.id);
}}
tooltip={isZh ? '资源监测' : 'Resource Monitor'}
aria-label={isZh ? '资源监测' : 'Resource Monitor'}
>
<Gauge size={14} />
</LucentIconButton>
{bot.docker_status === 'RUNNING' ? (
<LucentTooltip content={t.stop}>
<button
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);
}}
aria-label={t.stop}
>
{isStopping ? (
<span className="ops-control-pending">
<span className="ops-control-dots" aria-hidden="true">
<i />
<i />
<i />
</span>
</span>
) : <Square size={14} />}
</button>
</LucentTooltip>
) : (
<LucentTooltip content={t.start}>
<button
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);
}}
aria-label={t.start}
>
{isStarting ? (
<span className="ops-control-pending">
<span className="ops-control-dots" aria-hidden="true">
<i />
<i />
<i />
</span>
</span>
) : <Power size={14} />}
</button>
</LucentTooltip>
)}
<LucentTooltip content={t.delete}>
<button
className="btn btn-sm ops-bot-icon-btn ops-bot-action-delete"
<LucentIconButton
className="btn btn-sm ops-bot-icon-btn ops-bot-action-stop"
disabled={isOperating}
onClick={(e) => {
e.stopPropagation();
void removeBot(bot.id);
void stopBot(bot.id, bot.docker_status);
}}
aria-label={t.delete}
tooltip={t.stop}
aria-label={t.stop}
>
<Trash2 size={14} />
</button>
</LucentTooltip>
{isStopping ? (
<span className="ops-control-pending">
<span className="ops-control-dots" aria-hidden="true">
<i />
<i />
<i />
</span>
</span>
) : <Square size={14} />}
</LucentIconButton>
) : (
<LucentIconButton
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);
}}
tooltip={t.start}
aria-label={t.start}
>
{isStarting ? (
<span className="ops-control-pending">
<span className="ops-control-dots" aria-hidden="true">
<i />
<i />
<i />
</span>
</span>
) : <Power size={14} />}
</LucentIconButton>
)}
<LucentIconButton
className="btn btn-sm ops-bot-icon-btn ops-bot-action-delete"
onClick={(e) => {
e.stopPropagation();
void removeBot(bot.id);
}}
tooltip={t.delete}
aria-label={t.delete}
>
<Trash2 size={14} />
</LucentIconButton>
</div>
</div>
);
@ -2163,15 +2159,15 @@ export function BotDashboardModule({
: t.disabledPlaceholder
}
/>
<button
<LucentIconButton
className="btn btn-secondary icon-btn"
disabled={!canChat || isUploadingAttachments}
onClick={triggerPickAttachments}
title={isUploadingAttachments ? t.uploadingFile : t.uploadFile}
tooltip={isUploadingAttachments ? t.uploadingFile : t.uploadFile}
aria-label={isUploadingAttachments ? t.uploadingFile : t.uploadFile}
>
<Paperclip size={14} className={isUploadingAttachments ? 'animate-spin' : ''} />
</button>
</LucentIconButton>
<button
className="btn btn-primary"
disabled={!isChatEnabled || (!command.trim() && pendingAttachments.length === 0)}
@ -2210,18 +2206,18 @@ export function BotDashboardModule({
</a>
);
})()}
<button
<LucentIconButton
className="icon-btn ops-chip-remove"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setPendingAttachments((prev) => prev.filter((v) => v !== p));
}}
title={t.removeAttachment}
tooltip={t.removeAttachment}
aria-label={t.removeAttachment}
>
<X size={12} />
</button>
</LucentIconButton>
</span>
))}
</div>
@ -2254,24 +2250,24 @@ export function BotDashboardModule({
<div className="row-between ops-runtime-head">
<h2 style={{ fontSize: 18 }}>{t.runtime}</h2>
<div className="ops-panel-tools" ref={runtimeMenuRef}>
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setRuntimeViewMode((m) => (m === 'visual' ? 'text' : 'visual'))}
title={runtimeViewMode === 'visual' ? (isZh ? '切换为文字面板' : 'Switch to text panel') : (isZh ? '切换为机器人面板' : 'Switch to bot panel')}
tooltip={runtimeViewMode === 'visual' ? (isZh ? '切换为文字面板' : 'Switch to text panel') : (isZh ? '切换为机器人面板' : 'Switch to bot panel')}
aria-label={runtimeViewMode === 'visual' ? (isZh ? '切换为文字面板' : 'Switch to text panel') : (isZh ? '切换为机器人面板' : 'Switch to bot panel')}
>
<Repeat2 size={14} />
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setRuntimeMenuOpen((v) => !v)}
title={runtimeMoreLabel}
tooltip={runtimeMoreLabel}
aria-label={runtimeMoreLabel}
aria-haspopup="menu"
aria-expanded={runtimeMenuOpen}
>
<EllipsisVertical size={14} />
</button>
</LucentIconButton>
{runtimeMenuOpen ? (
<div className="ops-more-menu" role="menu" aria-label={runtimeMoreLabel}>
<button
@ -2412,14 +2408,14 @@ export function BotDashboardModule({
<div className="ops-runtime-action-inline">
<strong className="ops-runtime-action-text">{runtimeActionDisplay}</strong>
{runtimeActionHasMore ? (
<button
<LucentIconButton
className="ops-runtime-expand-btn"
onClick={() => setShowRuntimeActionModal(true)}
title={isZh ? '查看完整内容' : 'Show full content'}
tooltip={isZh ? '查看完整内容' : 'Show full content'}
aria-label={isZh ? '查看完整内容' : 'Show full content'}
>
</button>
</LucentIconButton>
) : null}
</div>
</div>
@ -2433,15 +2429,15 @@ export function BotDashboardModule({
<div className="section-mini-title">{t.workspaceOutputs}</div>
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
<div className="workspace-toolbar">
<button
<LucentIconButton
className="workspace-refresh-icon-btn"
disabled={workspaceLoading || !selectedBotId}
onClick={() => void loadWorkspaceTree(selectedBot.id, workspaceCurrentPath)}
title={lc.refreshHint}
tooltip={lc.refreshHint}
aria-label={lc.refreshHint}
>
<RefreshCw size={14} className={workspaceLoading ? 'animate-spin' : ''} />
</button>
</LucentIconButton>
<label className="workspace-auto-switch" title={lc.autoRefresh}>
<span className="workspace-auto-switch-label">{lc.autoRefresh}</span>
<input
@ -2487,14 +2483,14 @@ export function BotDashboardModule({
</section>
</div>
{compactMode && isCompactMobile ? (
<button
<LucentIconButton
className="ops-compact-fab-switch"
onClick={() => setCompactPanelTab((v) => (v === 'chat' ? 'runtime' : 'chat'))}
title={compactPanelTab === 'chat' ? (isZh ? '切换到运行面板' : 'Switch to runtime') : (isZh ? '切换到对话面板' : 'Switch to chat')}
tooltip={compactPanelTab === 'chat' ? (isZh ? '切换到运行面板' : 'Switch to runtime') : (isZh ? '切换到对话面板' : 'Switch to chat')}
aria-label={compactPanelTab === 'chat' ? (isZh ? '切换到运行面板' : 'Switch to runtime') : (isZh ? '切换到对话面板' : 'Switch to chat')}
>
{compactPanelTab === 'chat' ? <Activity size={18} /> : <MessageSquareText size={18} />}
</button>
</LucentIconButton>
) : null}
{showResourceModal && (
@ -2506,22 +2502,22 @@ export function BotDashboardModule({
<span className="modal-sub mono">{resourceBot?.name || resourceBotId}</span>
</div>
<div className="modal-title-actions">
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => void loadResourceSnapshot(resourceBotId)}
title={isZh ? '立即刷新' : 'Refresh now'}
tooltip={isZh ? '立即刷新' : 'Refresh now'}
aria-label={isZh ? '立即刷新' : 'Refresh now'}
>
<RefreshCw size={14} className={resourceLoading ? 'animate-spin' : ''} />
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setShowResourceModal(false)}
title={t.close}
tooltip={t.close}
aria-label={t.close}
>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
@ -2589,9 +2585,9 @@ export function BotDashboardModule({
<span className="modal-sub">{t.baseConfigSub}</span>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowBaseModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowBaseModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
@ -2661,9 +2657,9 @@ export function BotDashboardModule({
<h3>{t.modelParams}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowParamModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowParamModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="slider-row">
@ -2743,9 +2739,9 @@ export function BotDashboardModule({
<h3>{lc.wizardSectionTitle}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowChannelModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowChannelModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="card" style={{ fontSize: 12, color: 'var(--muted)' }}>
@ -2773,15 +2769,15 @@ export function BotDashboardModule({
/>
{lc.sendToolHints}
</label>
<button
<LucentIconButton
className="btn btn-primary btn-sm icon-btn"
disabled={isSavingGlobalDelivery || !selectedBot}
onClick={() => void saveGlobalDelivery()}
title={lc.saveChannel}
tooltip={lc.saveChannel}
aria-label={lc.saveChannel}
>
<Save size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="wizard-channel-list">
@ -2801,28 +2797,29 @@ export function BotDashboardModule({
/>
{lc.enabled}
</label>
<button
<LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn"
disabled={isDashboardChannel(channel) || isSavingChannel}
onClick={() => void removeChannel(channel)}
title={lc.remove}
tooltip={lc.remove}
aria-label={lc.remove}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
{renderChannelFields(channel, idx)}
<div className="row-between">
<span className="field-label">{lc.customChannel}</span>
<button
<LucentIconButton
className="btn btn-primary btn-sm icon-btn"
disabled={isSavingChannel}
onClick={() => void saveChannel(channel)}
title={lc.saveChannel}
tooltip={lc.saveChannel}
aria-label={lc.saveChannel}
>
<Save size={14} />
</button>
</LucentIconButton>
</div>
</div>
)
@ -2840,15 +2837,15 @@ export function BotDashboardModule({
<option key={t} value={t}>{t}</option>
))}
</select>
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
disabled={addableChannelTypes.length === 0 || isSavingChannel}
onClick={() => void addChannel()}
title={lc.addChannel}
tooltip={lc.addChannel}
aria-label={lc.addChannel}
>
<Plus size={14} />
</button>
</LucentIconButton>
</div>
</div>
</div>
@ -2862,9 +2859,9 @@ export function BotDashboardModule({
<h3>{t.skillsPanel}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowSkillsModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowSkillsModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="wizard-channel-list">
@ -2880,13 +2877,14 @@ export function BotDashboardModule({
<div className="field-label mono">{String(skill.type || '').toUpperCase()}</div>
<div className="field-label">{skill.description || '-'}</div>
</div>
<button
<LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn"
onClick={() => void removeBotSkill(skill)}
title={t.removeSkill}
tooltip={t.removeSkill}
aria-label={t.removeSkill}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
))
@ -2927,9 +2925,9 @@ export function BotDashboardModule({
<h3>{t.envParams}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowEnvParamsModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowEnvParamsModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="field-label" style={{ marginBottom: 8 }}>{t.envParamsDesc}</div>
@ -2948,21 +2946,22 @@ export function BotDashboardModule({
onChange={(e) => upsertEnvParam(key, e.target.value)}
placeholder={t.envValue}
/>
<button
<LucentIconButton
className="btn btn-secondary btn-sm wizard-icon-btn"
onClick={() => setEnvVisibleByKey((prev) => ({ ...prev, [key]: !prev[key] }))}
title={envVisibleByKey[key] ? t.hideEnvValue : t.showEnvValue}
tooltip={envVisibleByKey[key] ? t.hideEnvValue : t.showEnvValue}
aria-label={envVisibleByKey[key] ? t.hideEnvValue : t.showEnvValue}
>
{envVisibleByKey[key] ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn"
onClick={() => removeEnvParam(key)}
title={t.removeEnvParam}
tooltip={t.removeEnvParam}
aria-label={t.removeEnvParam}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
))
@ -2983,15 +2982,15 @@ export function BotDashboardModule({
onChange={(e) => setEnvDraftValue(e.target.value)}
placeholder={t.envValue}
/>
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setEnvDraftVisible((v) => !v)}
title={envDraftVisible ? t.hideEnvValue : t.showEnvValue}
tooltip={envDraftVisible ? t.hideEnvValue : t.showEnvValue}
aria-label={envDraftVisible ? t.hideEnvValue : t.showEnvValue}
>
{envDraftVisible ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => {
const key = String(envDraftKey || '').trim().toUpperCase();
@ -3000,11 +2999,11 @@ export function BotDashboardModule({
setEnvDraftKey('');
setEnvDraftValue('');
}}
title={t.addEnvParam}
tooltip={t.addEnvParam}
aria-label={t.addEnvParam}
>
<Plus size={14} />
</button>
</LucentIconButton>
</div>
<div className="row-between">
<span className="field-label">{t.envParamsHint}</span>
@ -3025,18 +3024,18 @@ export function BotDashboardModule({
<h3>{t.cronViewer}</h3>
</div>
<div className="modal-title-actions">
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => selectedBot && void loadCronJobs(selectedBot.id)}
title={t.cronReload}
tooltip={t.cronReload}
aria-label={t.cronReload}
disabled={cronLoading}
>
<RefreshCw size={14} className={cronLoading ? 'animate-spin' : ''} />
</button>
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowCronModal(false)} title={t.close} aria-label={t.close}>
</LucentIconButton>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowCronModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
{cronLoading ? (
@ -3067,24 +3066,24 @@ export function BotDashboardModule({
<div className="ops-cron-meta">{job.enabled === false ? t.cronDisabled : t.cronEnabled}</div>
</div>
<div className="ops-cron-actions">
<button
<LucentIconButton
className="btn btn-danger btn-sm icon-btn"
onClick={() => void stopCronJob(job.id)}
title={t.cronStop}
tooltip={t.cronStop}
aria-label={t.cronStop}
disabled={stopping || job.enabled === false}
>
<PowerOff size={13} />
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-danger btn-sm icon-btn"
onClick={() => void deleteCronJob(job.id)}
title={t.cronDelete}
tooltip={t.cronDelete}
aria-label={t.cronDelete}
disabled={stopping}
>
<Trash2 size={13} />
</button>
</LucentIconButton>
</div>
</div>
);
@ -3103,9 +3102,9 @@ export function BotDashboardModule({
<h3>{t.agentFiles}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowAgentModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowAgentModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="wizard-agent-layout">
@ -3132,9 +3131,9 @@ export function BotDashboardModule({
<h3>{t.lastAction}</h3>
</div>
<div className="modal-title-actions">
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowRuntimeActionModal(false)} title={t.close} aria-label={t.close}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowRuntimeActionModal(false)} tooltip={t.close} aria-label={t.close}>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className="workspace-preview-body">
@ -3153,22 +3152,22 @@ export function BotDashboardModule({
<span className="modal-sub mono">{workspacePreview.path}</span>
</div>
<div className="workspace-preview-header-actions">
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setWorkspacePreviewFullscreen((v) => !v)}
title={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
tooltip={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
aria-label={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
>
{workspacePreviewFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={closeWorkspacePreview}
title={t.close}
tooltip={t.close}
aria-label={t.close}
>
<X size={14} />
</button>
</LucentIconButton>
</div>
</div>
<div className={`workspace-preview-body ${workspacePreview.isMarkdown ? 'markdown' : ''}`}>

View File

@ -7,6 +7,7 @@ import { pickLocale } from '../../i18n';
import { imageFactoryZhCn } from '../../i18n/image-factory.zh-cn';
import { imageFactoryEn } from '../../i18n/image-factory.en';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
interface NanobotImage {
tag: string;
@ -178,15 +179,15 @@ export function ImageFactoryModule() {
<td>{img.version}</td>
<td><span className={statusClass(img.status)}>{img.status}</span></td>
<td>
<button
<LucentIconButton
className="btn btn-danger btn-sm icon-btn"
disabled={isDeletingTag === img.tag}
onClick={() => void handleDeleteRegistered(img.tag)}
title={isDeletingTag === img.tag ? t.deleting : t.deleteRegistry}
tooltip={isDeletingTag === img.tag ? t.deleting : t.deleteRegistry}
aria-label={isDeletingTag === img.tag ? t.deleting : t.deleteRegistry}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</td>
</tr>
))}
@ -202,14 +203,14 @@ export function ImageFactoryModule() {
<h2>{t.dockerTitle}</h2>
<p className="panel-desc">{t.dockerDesc}</p>
</div>
<button
<LucentIconButton
className="btn btn-secondary icon-btn"
onClick={() => void refreshDockerImages()}
title={isRefreshing ? t.refreshing : t.refresh}
tooltip={isRefreshing ? t.refreshing : t.refresh}
aria-label={isRefreshing ? t.refreshing : t.refresh}
>
<RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />
</button>
</LucentIconButton>
</div>
<div className="card" style={{ fontSize: 12, color: 'var(--muted)' }}>

View File

@ -7,6 +7,7 @@ import { pickLocale } from '../../../i18n';
import { managementZhCn } from '../../../i18n/management.zh-cn';
import { managementEn } from '../../../i18n/management.en';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
interface KernelManagerModalProps {
isOpen: boolean;
@ -65,9 +66,9 @@ export function KernelManagerModal({ isOpen, onClose }: KernelManagerModalProps)
<Cpu className="text-blue-400" size={24} />
<h2 className="text-xl font-bold text-white">{t.title}</h2>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-700 rounded-full transition-colors text-white">
<LucentIconButton onClick={onClose} className="p-2 hover:bg-slate-700 rounded-full transition-colors text-white" tooltip={locale === 'zh' ? '关闭' : 'Close'} aria-label={locale === 'zh' ? '关闭' : 'Close'}>
<X size={20} />
</button>
</LucentIconButton>
</div>
<div className="p-6">
@ -87,13 +88,14 @@ export function KernelManagerModal({ isOpen, onClose }: KernelManagerModalProps)
<span className={`text-[9px] font-bold ${img.status === 'READY' ? 'text-green-500' : 'text-slate-400'}`}>
{img.status}
</span>
<button
<LucentIconButton
onClick={() => handleRemoveImage(img.tag)}
className="ml-2 p-1.5 hover:bg-red-500/20 text-slate-500 hover:text-red-500 rounded transition-colors"
title={t.removeRecord}
tooltip={t.removeRecord}
aria-label={t.removeRecord}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
))}

View File

@ -9,6 +9,7 @@ import { pickLocale } from '../../i18n';
import { wizardZhCn } from '../../i18n/wizard.zh-cn';
import { wizardEn } from '../../i18n/wizard.en';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
@ -698,9 +699,9 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
<div className="mono">
{configuredChannelsLabel}
</div>
<button className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowChannelModal(true)} title={lc.openManager} aria-label={lc.openManager}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowChannelModal(true)} tooltip={lc.openManager} aria-label={lc.openManager}>
<Settings2 size={14} />
</button>
</LucentIconButton>
</div>
<div className="section-mini-title" style={{ marginTop: 6 }}>{ui.toolsConfig}</div>
@ -709,14 +710,14 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
<div className="mono">
{envEntries.length > 0 ? envEntries.map(([k]) => k).join(', ') : ui.noEnvParams}
</div>
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setShowToolsConfigModal(true)}
title={ui.openToolsManager}
tooltip={ui.openToolsManager}
aria-label={ui.openToolsManager}
>
<Settings2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
</div>
@ -825,13 +826,14 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
/>
{lc.enabled}
</label>
<button
<LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn"
onClick={() => removeChannel(idx)}
title={lc.remove}
tooltip={lc.remove}
aria-label={lc.remove}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
@ -846,9 +848,9 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
<option key={t} value={t}>{t}</option>
))}
</select>
<button className="btn btn-secondary btn-sm icon-btn" disabled={addableChannelTypes.length === 0} onClick={addChannel} title={lc.addChannel} aria-label={lc.addChannel}>
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" disabled={addableChannelTypes.length === 0} onClick={addChannel} tooltip={lc.addChannel} aria-label={lc.addChannel}>
<Plus size={14} />
</button>
</LucentIconButton>
</div>
<div className="row-between">
<span className="field-label">{lc.wizardSectionDesc}</span>
@ -883,21 +885,22 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
onChange={(e) => upsertEnvParam(key, e.target.value)}
placeholder={ui.envValue}
/>
<button
<LucentIconButton
className="btn btn-secondary btn-sm wizard-icon-btn"
onClick={() => setEnvVisibleByKey((prev) => ({ ...prev, [key]: !prev[key] }))}
title={envVisibleByKey[key] ? ui.hideEnvValue : ui.showEnvValue}
tooltip={envVisibleByKey[key] ? ui.hideEnvValue : ui.showEnvValue}
aria-label={envVisibleByKey[key] ? ui.hideEnvValue : ui.showEnvValue}
>
{envVisibleByKey[key] ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn"
onClick={() => removeEnvParam(key)}
title={ui.removeEnvParam}
tooltip={ui.removeEnvParam}
aria-label={ui.removeEnvParam}
>
<Trash2 size={14} />
</button>
</LucentIconButton>
</div>
</div>
))
@ -917,15 +920,15 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
onChange={(e) => setEnvDraftValue(e.target.value)}
placeholder={ui.envValue}
/>
<button
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => setEnvDraftVisible((v) => !v)}
title={envDraftVisible ? ui.hideEnvValue : ui.showEnvValue}
tooltip={envDraftVisible ? ui.hideEnvValue : ui.showEnvValue}
aria-label={envDraftVisible ? ui.hideEnvValue : ui.showEnvValue}
>
{envDraftVisible ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
<button
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
onClick={() => {
const key = String(envDraftKey || '').trim().toUpperCase();
@ -934,11 +937,11 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
setEnvDraftKey('');
setEnvDraftValue('');
}}
title={ui.addEnvParam}
tooltip={ui.addEnvParam}
aria-label={ui.addEnvParam}
>
<Plus size={14} />
</button>
</LucentIconButton>
</div>
<div className="row-between">
<span className="field-label">{ui.toolsDesc}</span>