diff --git a/backend/.env.example b/backend/.env.example index 0ee3773..df0bda9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -27,16 +27,11 @@ PANEL_ACCESS_PASSWORD= # The following platform-level items are now managed in sys_setting / 平台参数: # - page_size # - chat_pull_page_size +# - command_auto_unlock_seconds # - upload_max_mb # - allowed_attachment_extensions # - workspace_download_extensions # - speech_enabled -# - speech_max_audio_seconds -# - speech_default_language -# - speech_force_simplified -# - speech_audio_preprocess -# - speech_audio_filter -# - speech_initial_prompt # Local speech-to-text (Whisper via whisper.cpp model file) STT_MODEL=ggml-small-q8_0.bin diff --git a/backend/api/image_router.py b/backend/api/image_router.py index 7083a0a..333c5d4 100644 --- a/backend/api/image_router.py +++ b/backend/api/image_router.py @@ -45,11 +45,6 @@ def _reconcile_registered_images(session: Session) -> None: session.commit() -def reconcile_image_registry(session: Session) -> None: - """Backward-compatible alias for older callers after router refactor.""" - _reconcile_registered_images(session) - - @router.get("/api/images") def list_images(session: Session = Depends(get_session)): cached = cache.get_json(_cache_key_images()) diff --git a/backend/api/system_router.py b/backend/api/system_router.py index c2a8a0b..8791b27 100644 --- a/backend/api/system_router.py +++ b/backend/api/system_router.py @@ -52,7 +52,6 @@ def get_system_defaults(): "bot": { "system_timezone": _get_default_system_timezone(), }, - "loading_page": platform_settings.loading_page.model_dump(), "chat": { "pull_page_size": platform_settings.chat_pull_page_size, "page_size": platform_settings.page_size, diff --git a/backend/schemas/platform.py b/backend/schemas/platform.py index ed79fe2..3f43d8c 100644 --- a/backend/schemas/platform.py +++ b/backend/schemas/platform.py @@ -3,12 +3,6 @@ from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field -class LoadingPageSettings(BaseModel): - title: str = "Dashboard Nanobot" - subtitle: str = "平台正在准备管理面板" - description: str = "请稍候,正在加载 Bot 平台数据。" - - class PlatformSettingsPayload(BaseModel): page_size: int = Field(default=10, ge=1, le=100) chat_pull_page_size: int = Field(default=60, ge=10, le=500) @@ -17,15 +11,6 @@ class PlatformSettingsPayload(BaseModel): allowed_attachment_extensions: List[str] = Field(default_factory=list) workspace_download_extensions: List[str] = Field(default_factory=list) speech_enabled: bool = True - speech_max_audio_seconds: int = Field(default=20, ge=5, le=600) - speech_default_language: str = Field(default="zh", min_length=1, max_length=16) - speech_force_simplified: bool = True - speech_audio_preprocess: bool = True - speech_audio_filter: str = Field(default="highpass=f=120,lowpass=f=7600,afftdn=nf=-20") - speech_initial_prompt: str = Field( - default="以下内容可能包含简体中文和英文术语。请优先输出简体中文,英文单词、缩写、品牌名和数字保持原文,不要翻译。" - ) - loading_page: LoadingPageSettings = Field(default_factory=LoadingPageSettings) class PlatformUsageItem(BaseModel): diff --git a/backend/services/platform_runtime_settings_service.py b/backend/services/platform_runtime_settings_service.py index 4def569..322a863 100644 --- a/backend/services/platform_runtime_settings_service.py +++ b/backend/services/platform_runtime_settings_service.py @@ -14,7 +14,7 @@ from core.settings import ( STT_MODEL, ) from models.platform import PlatformSetting -from schemas.platform import LoadingPageSettings, PlatformSettingsPayload +from schemas.platform import PlatformSettingsPayload from services.platform_settings_core import ( SETTING_KEYS, SYSTEM_SETTING_DEFINITIONS, @@ -36,13 +36,6 @@ def default_platform_settings() -> PlatformSettingsPayload: allowed_attachment_extensions=list(bootstrap["allowed_attachment_extensions"]), workspace_download_extensions=list(bootstrap["workspace_download_extensions"]), speech_enabled=bool(bootstrap["speech_enabled"]), - speech_max_audio_seconds=DEFAULT_STT_MAX_AUDIO_SECONDS, - speech_default_language=DEFAULT_STT_DEFAULT_LANGUAGE, - speech_force_simplified=DEFAULT_STT_FORCE_SIMPLIFIED, - speech_audio_preprocess=DEFAULT_STT_AUDIO_PREPROCESS, - speech_audio_filter=DEFAULT_STT_AUDIO_FILTER, - speech_initial_prompt=DEFAULT_STT_INITIAL_PROMPT, - loading_page=LoadingPageSettings(), ) @@ -67,14 +60,6 @@ def get_platform_settings(session: Session) -> PlatformSettingsPayload: data.get("workspace_download_extensions", merged["workspace_download_extensions"]) ) merged["speech_enabled"] = bool(data.get("speech_enabled", merged["speech_enabled"])) - loading_page = data.get("loading_page") - if isinstance(loading_page, dict): - current = dict(merged["loading_page"]) - for key in ("title", "subtitle", "description"): - value = str(loading_page.get(key) or "").strip() - if value: - current[key] = value - merged["loading_page"] = current return PlatformSettingsPayload.model_validate(merged) @@ -87,7 +72,6 @@ def save_platform_settings(session: Session, payload: PlatformSettingsPayload) - allowed_attachment_extensions=_normalize_extension_list(payload.allowed_attachment_extensions), workspace_download_extensions=_normalize_extension_list(payload.workspace_download_extensions), speech_enabled=bool(payload.speech_enabled), - loading_page=LoadingPageSettings.model_validate(payload.loading_page.model_dump()), ) payload_by_key = normalized.model_dump() for key in SETTING_KEYS: diff --git a/frontend/public/nanobot-logo.png b/frontend/public/nanobot-logo.png deleted file mode 100644 index 9493813..0000000 Binary files a/frontend/public/nanobot-logo.png and /dev/null differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/DrawerShell.css b/frontend/src/components/DrawerShell.css index 496ac94..e6aefc4 100644 --- a/frontend/src/components/DrawerShell.css +++ b/frontend/src/components/DrawerShell.css @@ -18,6 +18,8 @@ .drawer-shell { height: 100dvh; max-width: 100vw; + padding: 10px 0 10px 14px; + box-sizing: border-box; } .drawer-shell.drawer-shell-standard { @@ -29,16 +31,26 @@ } .drawer-shell-surface { + position: relative; + isolation: isolate; display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; + overflow: hidden; background: - linear-gradient(180deg, color-mix(in oklab, var(--panel) 96%, var(--brand-soft) 4%), var(--panel)), + radial-gradient(circle at top left, color-mix(in oklab, var(--brand-soft) 34%, transparent), transparent 30%), + radial-gradient(circle at left center, color-mix(in oklab, var(--brand-soft) 16%, transparent), transparent 36%), + linear-gradient(180deg, color-mix(in oklab, var(--panel) 95%, var(--brand-soft) 5%), color-mix(in oklab, var(--panel-soft) 98%, transparent)), var(--panel); - border-left: 1px solid color-mix(in oklab, var(--line) 78%, transparent); - box-shadow: -18px 0 42px rgba(13, 24, 45, 0.22); + border: 1px solid color-mix(in oklab, var(--line) 74%, transparent); + border-right: 0; + border-top-left-radius: 24px; + border-bottom-left-radius: 24px; + box-shadow: + -24px 0 54px rgba(13, 24, 45, 0.2), + -1px 0 0 color-mix(in oklab, var(--brand) 8%, transparent); opacity: 0.98; transform: translateX(56px); transition: @@ -48,6 +60,23 @@ will-change: transform, opacity; } +.drawer-shell-surface::before { + content: ''; + position: absolute; + inset: 16px auto 16px 0; + width: 2px; + border-radius: 999px; + background: linear-gradient( + 180deg, + color-mix(in oklab, var(--brand) 42%, transparent), + color-mix(in oklab, var(--brand-soft) 24%, transparent) + ); + box-shadow: 0 0 0 1px color-mix(in oklab, var(--brand) 8%, transparent); + opacity: 0.84; + pointer-events: none; + z-index: 1; +} + .drawer-shell-surface.is-open { opacity: 1; transform: translateX(0); @@ -56,7 +85,9 @@ .drawer-shell-header, .drawer-shell-footer { flex: 0 0 auto; - background: inherit; + position: relative; + z-index: 2; + background: transparent; } .drawer-shell-header { @@ -104,6 +135,8 @@ overflow: auto; padding: 20px 24px 24px; overscroll-behavior: contain; + position: relative; + z-index: 2; } .drawer-shell-footer { @@ -137,6 +170,7 @@ .drawer-shell.drawer-shell-standard, .drawer-shell.drawer-shell-extend { width: 100vw; + padding: 0; } .drawer-shell-header, @@ -152,6 +186,17 @@ .drawer-shell-header { padding-top: calc(18px + env(safe-area-inset-top, 0px)); } + + .drawer-shell-surface { + border-radius: 0; + border-inline: 0; + border-block: 0; + box-shadow: none; + } + + .drawer-shell-surface::before { + display: none; + } } @media (prefers-reduced-motion: reduce) { diff --git a/frontend/src/components/lucent/LucentTooltip.tsx b/frontend/src/components/lucent/LucentTooltip.tsx index 275f038..eea9d93 100644 --- a/frontend/src/components/lucent/LucentTooltip.tsx +++ b/frontend/src/components/lucent/LucentTooltip.tsx @@ -41,8 +41,9 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip const child = useMemo(() => { const first = Children.only(children) as ReactNode; - return isValidElement(first) ? (first as ReactElement<{ 'aria-describedby'?: string }>) : null; + return isValidElement(first) ? (first as ReactElement<{ 'aria-describedby'?: string; disabled?: boolean }>) : null; }, [children]); + const childDisabled = Boolean(child?.props.disabled); const updatePosition = useCallback(() => { const wrap = wrapRef.current; @@ -104,14 +105,32 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip useEffect(() => { if (!visible) return; const handleWindowChange = () => updatePosition(); + const handleDismiss = () => setVisible(false); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setVisible(false); + } + }; window.addEventListener('scroll', handleWindowChange, true); window.addEventListener('resize', handleWindowChange); + window.addEventListener('blur', handleDismiss); + document.addEventListener('pointerdown', handleDismiss, true); + document.addEventListener('keydown', handleKeyDown, true); return () => { window.removeEventListener('scroll', handleWindowChange, true); window.removeEventListener('resize', handleWindowChange); + window.removeEventListener('blur', handleDismiss); + document.removeEventListener('pointerdown', handleDismiss, true); + document.removeEventListener('keydown', handleKeyDown, true); }; }, [updatePosition, visible]); + useEffect(() => { + if (childDisabled) { + setVisible(false); + } + }, [childDisabled]); + if (!text) return <>{children}; const enhancedChild = child @@ -127,7 +146,14 @@ export function LucentTooltip({ content, children, side = 'top' }: LucentTooltip className="lucent-tooltip-wrap" onMouseEnter={() => setVisible(true)} onMouseLeave={() => setVisible(false)} + onPointerDownCapture={() => setVisible(false)} + onClickCapture={() => setVisible(false)} onFocusCapture={() => setVisible(true)} + onKeyDownCapture={(event) => { + if (event.key === 'Enter' || event.key === ' ' || event.key === 'Escape') { + setVisible(false); + } + }} onBlurCapture={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { setVisible(false); diff --git a/frontend/src/components/skill-market/SkillMarketShared.css b/frontend/src/components/skill-market/SkillMarketShared.css index ba895f1..cb4c8d9 100644 --- a/frontend/src/components/skill-market/SkillMarketShared.css +++ b/frontend/src/components/skill-market/SkillMarketShared.css @@ -275,10 +275,46 @@ max-width: 720px; } -.skill-market-drawer-body { +.skill-market-form-drawer-body { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; padding-bottom: 28px; } +.skill-market-form-modal { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.skill-market-form-scroll { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + padding-right: 4px; +} + +.skill-market-inline-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.skill-market-inline-actions-wrap { + flex-wrap: wrap; +} + +.skill-market-button-with-icon { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; +} + .skill-market-editor-textarea { min-height: 180px; } diff --git a/frontend/src/i18n/dashboard.en.ts b/frontend/src/i18n/dashboard.en.ts index 971e944..5b33197 100644 --- a/frontend/src/i18n/dashboard.en.ts +++ b/frontend/src/i18n/dashboard.en.ts @@ -54,6 +54,12 @@ export const dashboardEn = { quotedReplyLabel: 'Quoted reply', clearQuote: 'Clear quote', quoteOnlyMessage: '[quoted reply]', + stagedSubmissionEmpty: 'Attachments or quoted reply only', + stagedSubmissionRestore: 'Edit', + stagedSubmissionRemove: 'Remove', + stagedSubmissionQueued: 'Added to the staged queue. It will auto-send after earlier tasks finish.', + stagedSubmissionRestored: 'Staged prompt restored to the composer.', + stagedSubmissionAttachmentCount: (count: number) => `${count} attachment${count === 1 ? '' : 's'}`, goodReply: 'Good reply', badReply: 'Bad reply', feedbackUpSaved: 'Marked as good reply.', diff --git a/frontend/src/i18n/dashboard.zh-cn.ts b/frontend/src/i18n/dashboard.zh-cn.ts index 3534401..fa9ea4e 100644 --- a/frontend/src/i18n/dashboard.zh-cn.ts +++ b/frontend/src/i18n/dashboard.zh-cn.ts @@ -54,6 +54,12 @@ export const dashboardZhCn = { quotedReplyLabel: '已引用回复', clearQuote: '取消引用', quoteOnlyMessage: '[引用回复]', + stagedSubmissionEmpty: '仅包含附件或引用内容', + stagedSubmissionRestore: '编辑', + stagedSubmissionRemove: '移除', + stagedSubmissionQueued: '已加入暂存队列,前序任务结束后会自动提交。', + stagedSubmissionRestored: '暂存内容已恢复到输入框。', + stagedSubmissionAttachmentCount: (count: number) => `附件 ${count}`, goodReply: '好回复', badReply: '坏回复', feedbackUpSaved: '已标记为好回复。', diff --git a/frontend/src/modules/dashboard/BotDashboardModule.css b/frontend/src/modules/dashboard/BotDashboardModule.css index 60ce426..c585495 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.css +++ b/frontend/src/modules/dashboard/BotDashboardModule.css @@ -121,11 +121,6 @@ background: color-mix(in oklab, var(--panel) 92%, white 8%); } -.ops-chat-topic-frame { - min-height: 0; - overflow: hidden; -} - .ops-main-mode-tab { position: relative; display: flex; @@ -221,41 +216,8 @@ grid-template-rows: auto auto minmax(0, 1fr) auto; } -.ops-status-group { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.ops-status-pill { - border: 1px solid var(--line); - border-radius: 999px; - padding: 5px 10px; - font-size: 12px; - font-weight: 800; - color: var(--text); - background: var(--panel); -} - -.ops-status-pill.running { - border-color: #2ca476; - background: rgba(44, 164, 118, 0.16); -} - -.ops-status-pill.stopped, -.ops-status-pill.exited { - border-color: #d28686; - background: rgba(209, 75, 75, 0.16); -} - -.ops-status-pill.error { - border-color: #d14b4b; - background: rgba(209, 75, 75, 0.2); -} - -.ops-status-pill.idle { - border-color: #4b79d4; - background: rgba(47, 105, 226, 0.16); +.ops-panel-empty-copy { + color: var(--muted); } .ops-switch-dot { @@ -319,7 +281,3 @@ 0 16px 32px rgba(63, 116, 223, 0.26), inset 0 0 0 1px rgba(63, 116, 223, 0.78); } - -.app-shell[data-theme='light'] .ops-status-pill { - background: #ffffff; -} diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index fa8d3b7..9ed9049 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -132,6 +132,10 @@ export function BotDashboardModule({ interrupt: dashboard.t.interrupt, noConversation: dashboard.t.noConversation, previewTitle: dashboard.t.previewTitle, + stagedSubmissionAttachmentCount: dashboard.t.stagedSubmissionAttachmentCount, + stagedSubmissionEmpty: dashboard.t.stagedSubmissionEmpty, + stagedSubmissionRestore: dashboard.t.stagedSubmissionRestore, + stagedSubmissionRemove: dashboard.t.stagedSubmissionRemove, quoteReply: dashboard.t.quoteReply, quotedReplyLabel: dashboard.t.quotedReplyLabel, send: dashboard.t.send, @@ -169,6 +173,9 @@ export function BotDashboardModule({ selectedBotControlState: dashboard.selectedBotControlState, quotedReply: dashboard.quotedReply, onClearQuotedReply: () => dashboard.setQuotedReply(null), + stagedSubmissions: dashboard.selectedBotStagedSubmissions, + onRestoreStagedSubmission: dashboard.restoreStagedSubmission, + onRemoveStagedSubmission: dashboard.removeStagedSubmission, pendingAttachments: dashboard.pendingAttachments, onRemovePendingAttachment: (path: string) => dashboard.setPendingAttachments((prev) => prev.filter((value) => value !== path)), @@ -208,8 +215,8 @@ export function BotDashboardModule({ voiceCountdown: dashboard.voiceCountdown, onVoiceInput: dashboard.onVoiceInput, onTriggerPickAttachments: dashboard.triggerPickAttachments, - showInterruptSubmitAction: dashboard.showInterruptSubmitAction, - onSubmitAction: () => (dashboard.showInterruptSubmitAction ? dashboard.interruptExecution() : dashboard.send()), + submitActionMode: dashboard.submitActionMode, + onSubmitAction: dashboard.handlePrimarySubmitAction, }; const runtimePanelProps = { @@ -230,7 +237,6 @@ export function BotDashboardModule({ workspaceFileLoading: dashboard.workspaceFileLoading, workspaceDownloadExtensionSet: dashboard.workspaceDownloadExtensionSet, workspaceAutoRefresh: dashboard.workspaceAutoRefresh, - hasPreviewFiles: dashboard.workspaceFiles.length > 0, isCompactHidden: dashboard.compactMode && (dashboard.isCompactListPage || dashboard.compactPanelTab !== 'runtime'), showCompactSurface: dashboard.showCompactBotPageClose, emptyStateText: dashboard.forcedBotMissing ? `${dashboard.t.noTelemetry}: ${String(dashboard.forcedBotId).trim()}` : dashboard.t.noTelemetry, diff --git a/frontend/src/modules/dashboard/components/BotDashboardView.tsx b/frontend/src/modules/dashboard/components/BotDashboardView.tsx index ef77171..7da170d 100644 --- a/frontend/src/modules/dashboard/components/BotDashboardView.tsx +++ b/frontend/src/modules/dashboard/components/BotDashboardView.tsx @@ -98,7 +98,7 @@ export function BotDashboardView({ ) : ( -
{selectBotText}
+
{selectBotText}
)} diff --git a/frontend/src/modules/dashboard/components/DashboardChannelTopicModals.tsx b/frontend/src/modules/dashboard/components/DashboardChannelTopicModals.tsx index 3f4669b..a145c63 100644 --- a/frontend/src/modules/dashboard/components/DashboardChannelTopicModals.tsx +++ b/frontend/src/modules/dashboard/components/DashboardChannelTopicModals.tsx @@ -571,7 +571,7 @@ export function ChannelConfigModal({
{labels.channelAddHint} -
+
{labels.topicAddHint} -
+
+ +
+
+ ))} +
+ ) : null} {(quotedReply || pendingAttachments.length > 0) ? (
{quotedReply ? ( @@ -331,7 +390,7 @@ export function DashboardChatPanel({ multiple accept={allowedAttachmentExtensions.length > 0 ? allowedAttachmentExtensions.join(',') : undefined} onChange={onPickAttachments} - style={{ display: 'none' }} + className="ops-hidden-file-input" />
@@ -412,7 +471,7 @@ export function DashboardChatPanel({ onClick={() => void onJumpConversationToDate()} > {chatDateJumping ? : null} - + {isZh ? '跳转' : 'Jump'} @@ -488,13 +547,21 @@ export function DashboardChatPanel({
)} > -
-
{isZh ? '基础信息' : 'Basic Info'}
- - +
+
+
+
{isZh ? '基础信息' : 'Basic Info'}
+ + - - onEditFormChange({ name: e.target.value })} placeholder={labels.botNamePlaceholder} /> + + onEditFormChange({ name: e.target.value })} placeholder={labels.botNamePlaceholder} /> - - onEditFormChange({ access_password: e.target.value })} - placeholder={labels.accessPasswordPlaceholder} - toggleLabels={passwordToggleLabels} - /> + + onEditFormChange({ access_password: e.target.value })} + placeholder={labels.accessPasswordPlaceholder} + toggleLabels={passwordToggleLabels} + /> - - onEditFormChange({ image_tag: e.target.value })}> - {baseImageOptions.map((img) => ( - - ))} - + + onEditFormChange({ image_tag: e.target.value })}> + {baseImageOptions.map((img) => ( + + ))} + - - onEditFormChange({ system_timezone: e.target.value })} - > - {systemTimezoneOptions.map((option) => ( - - ))} - -
- {isZh ? '提示:系统时区保存后,在 Bot 重启后生效。' : 'Tip: timezone changes take effect after the bot restarts.'} + + onEditFormChange({ system_timezone: e.target.value })} + > + {systemTimezoneOptions.map((option) => ( + + ))} + +
+ {isZh ? '提示:系统时区保存后,在 Bot 重启后生效。' : 'Tip: timezone changes take effect after the bot restarts.'} +
+ +
+ {isZh ? '硬件资源' : 'Hardware Resources'} +
+ + onParamDraftChange({ cpu_cores: e.target.value })} + /> + + onParamDraftChange({ memory_mb: e.target.value })} + /> + + onParamDraftChange({ storage_gb: e.target.value })} + /> +
{isZh ? '提示:填写 0 表示不限制(保存后需手动重启 Bot 生效)。' : 'Tip: value 0 means unlimited (takes effect after manual bot restart).'}
+
- -
- {isZh ? '硬件资源' : 'Hardware Resources'} -
- - onParamDraftChange({ cpu_cores: e.target.value })} - /> - - onParamDraftChange({ memory_mb: e.target.value })} - /> - - onParamDraftChange({ storage_gb: e.target.value })} - /> -
{isZh ? '提示:填写 0 表示不限制(保存后需手动重启 Bot 生效)。' : 'Tip: value 0 means unlimited (takes effect after manual bot restart).'}
); @@ -327,6 +328,7 @@ export function ParamConfigModal({ title={labels.modelParams} subtitle={isZh ? '维护 Provider、模型、密钥和采样参数。' : 'Maintain the provider, model, key, and sampling parameters.'} size="standard" + bodyClassName="ops-form-drawer-body" closeLabel={labels.close} footer={(
@@ -335,76 +337,80 @@ export function ParamConfigModal({ ? labels.testing : (providerTestResult || (isZh ? '保存后模型参数会同步到当前 Bot。' : 'Saving syncs the latest model parameters to the current bot.'))}
-
+
)} > -
- - onProviderChange(e.target.value)}> - - {providerOptions.map((option) => ( - - ))} - +
+
+
+ + onProviderChange(e.target.value)}> + + {providerOptions.map((option) => ( + + ))} + - - onEditFormChange({ llm_model: e.target.value })} placeholder={labels.modelNamePlaceholder} /> + + onEditFormChange({ llm_model: e.target.value })} placeholder={labels.modelNamePlaceholder} /> - - onEditFormChange({ api_key: e.target.value })} - placeholder={labels.newApiKeyPlaceholder} - toggleLabels={passwordToggleLabels} - /> + + onEditFormChange({ api_key: e.target.value })} + placeholder={labels.newApiKeyPlaceholder} + toggleLabels={passwordToggleLabels} + /> - - onEditFormChange({ api_base: e.target.value })} placeholder="API Base URL" /> + + onEditFormChange({ api_base: e.target.value })} placeholder="API Base URL" /> - - {providerTestResult ?
{providerTestResult}
: null} - -
- - onEditFormChange({ temperature: clampTemperature(Number(e.target.value)) })} /> -
-
- - onEditFormChange({ top_p: Number(e.target.value) })} /> -
- - onParamDraftChange({ max_tokens: e.target.value })} - /> -
- {[4096, 8192, 16384, 32768].map((value) => ( - - ))} + {providerTestResult ?
{providerTestResult}
: null} + +
+ + onEditFormChange({ temperature: clampTemperature(Number(e.target.value)) })} /> +
+
+ + onEditFormChange({ top_p: Number(e.target.value) })} /> +
+ + onParamDraftChange({ max_tokens: e.target.value })} + /> +
+ {[4096, 8192, 16384, 32768].map((value) => ( + + ))} +
+
diff --git a/frontend/src/modules/dashboard/components/DashboardManagementModals.css b/frontend/src/modules/dashboard/components/DashboardManagementModals.css index 19f3d2c..0221a6d 100644 --- a/frontend/src/modules/dashboard/components/DashboardManagementModals.css +++ b/frontend/src/modules/dashboard/components/DashboardManagementModals.css @@ -1,13 +1,46 @@ -.ops-skills-list-scroll { - max-height: min(56vh, 520px); +.ops-form-drawer-body { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.ops-form-modal { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.ops-form-scroll { + flex: 1 1 auto; + min-height: 0; overflow: auto; padding-right: 4px; } -.ops-modal-scrollable { - max-height: min(92vh, 860px); +.ops-skills-drawer-body { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.ops-skills-modal { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.ops-skills-list-scroll { + flex: 1 1 auto; + min-height: 0; + max-height: none; overflow: auto; - overscroll-behavior: contain; + padding-right: 4px; } .ops-config-modal { @@ -156,49 +189,17 @@ flex-wrap: wrap; } -.ops-config-weixin-login-hint, -.ops-config-weixin-login-note { +.ops-config-weixin-login-hint { color: var(--muted); font-size: 12px; line-height: 1.5; } -.ops-config-weixin-login-status { - color: var(--text); - font-size: 13px; - line-height: 1.5; -} - .ops-config-weixin-login-body { display: grid; gap: 10px; } -.ops-config-weixin-qr-frame { - width: min(100%, 240px); - aspect-ratio: 1; - border-radius: 14px; - padding: 12px; - background: rgba(255, 255, 255, 0.96); - border: 1px solid color-mix(in oklab, var(--line) 72%, white 28%); -} - -.ops-config-weixin-qr { - width: 100%; - height: 100%; - object-fit: contain; - display: block; - border-radius: 10px; - background: #fff; -} - -.ops-config-weixin-login-url-label { - color: var(--muted); - font-size: 11px; - letter-spacing: 0.04em; - text-transform: uppercase; -} - .ops-config-weixin-login-url { padding: 10px 12px; border-radius: 10px; @@ -236,15 +237,6 @@ gap: 12px; } -.ops-skill-add-bar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding-top: 10px; - border-top: 1px solid color-mix(in oklab, var(--line) 82%, transparent); -} - .ops-skill-add-hint { flex: 1 1 auto; min-width: 0; @@ -262,6 +254,20 @@ flex: 0 0 auto; } +.ops-inline-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.ops-inline-actions-wrap { + flex-wrap: wrap; +} + +.ops-inline-actions-end { + justify-content: flex-end; +} + .ops-topic-create-menu { position: absolute; right: 0; @@ -313,11 +319,6 @@ max-width: 100%; } - .ops-skill-add-bar { - flex-direction: column; - align-items: stretch; - } - .ops-skill-create-trigger { width: 100%; justify-content: center; diff --git a/frontend/src/modules/dashboard/components/DashboardModalCardShell.tsx b/frontend/src/modules/dashboard/components/DashboardModalCardShell.tsx new file mode 100644 index 0000000..4f45639 --- /dev/null +++ b/frontend/src/modules/dashboard/components/DashboardModalCardShell.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from 'react'; +import { X } from 'lucide-react'; + +import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; + +interface DashboardModalCardShellProps { + cardClassName?: string; + children: ReactNode; + closeLabel: string; + headerActions?: ReactNode; + onClose: () => void; + subtitle?: ReactNode; + title: ReactNode; +} + +function joinClassNames(...values: Array) { + return values.filter(Boolean).join(' '); +} + +export function DashboardModalCardShell({ + cardClassName, + children, + closeLabel, + headerActions, + onClose, + subtitle, + title, +}: DashboardModalCardShellProps) { + return ( +
+
event.stopPropagation()}> +
+
+

{title}

+ {subtitle ? {subtitle} : null} +
+
+ {headerActions} + + + +
+
+ {children} +
+
+ ); +} diff --git a/frontend/src/modules/dashboard/components/DashboardPreviewModalShell.tsx b/frontend/src/modules/dashboard/components/DashboardPreviewModalShell.tsx new file mode 100644 index 0000000..b0c567e --- /dev/null +++ b/frontend/src/modules/dashboard/components/DashboardPreviewModalShell.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from 'react'; +import { X } from 'lucide-react'; + +import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; + +interface DashboardPreviewModalShellProps { + cardClassName?: string; + children: ReactNode; + closeLabel: string; + headerActions?: ReactNode; + onClose: () => void; + subtitle?: ReactNode; + title: ReactNode; +} + +function joinClassNames(...values: Array) { + return values.filter(Boolean).join(' '); +} + +export function DashboardPreviewModalShell({ + cardClassName, + children, + closeLabel, + headerActions, + onClose, + subtitle, + title, +}: DashboardPreviewModalShellProps) { + return ( +
+
event.stopPropagation()}> +
+
+

{title}

+ {subtitle ? {subtitle} : null} +
+
+ {headerActions} + + + +
+
+ {children} +
+
+ ); +} diff --git a/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx b/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx index b5d07e6..7ec4765 100644 --- a/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx +++ b/frontend/src/modules/dashboard/components/DashboardSkillsMcpModals.tsx @@ -59,6 +59,7 @@ export function SkillsModal({ title={labels.skillsPanel} subtitle={isZh ? '查看当前 Bot 已安装的技能。' : 'View the skills already installed for this bot.'} size="standard" + bodyClassName="ops-skills-drawer-body" closeLabel={labels.close} headerActions={( void onRefreshSkills()} tooltip={isZh ? '刷新已安装技能' : 'Refresh installed skills'} aria-label={isZh ? '刷新已安装技能' : 'Refresh installed skills'}> @@ -101,36 +102,34 @@ export function SkillsModal({
)} > -
-
-
-
{isZh ? '已安装技能' : 'Installed Skills'}
-
- {isZh ? `${botSkills.length} 个已安装` : `${botSkills.length} installed`} -
+
+
+
{isZh ? '已安装技能' : 'Installed Skills'}
+
+ {isZh ? `${botSkills.length} 个已安装` : `${botSkills.length} installed`}
+
-
- {botSkills.length === 0 ? ( -
{labels.skillsEmpty}
- ) : ( - botSkills.map((skill) => ( -
-
-
- {skill.name || skill.id} -
{skill.path}
-
{String(skill.type || '').toUpperCase()}
-
{skill.description || '-'}
-
- void onRemoveSkill(skill)} tooltip={labels.removeSkill} aria-label={labels.removeSkill}> - - +
+ {botSkills.length === 0 ? ( +
{labels.skillsEmpty}
+ ) : ( + botSkills.map((skill) => ( +
+
+
+ {skill.name || skill.id} +
{skill.path}
+
{String(skill.type || '').toUpperCase()}
+
{skill.description || '-'}
+ void onRemoveSkill(skill)} tooltip={labels.removeSkill} aria-label={labels.removeSkill}> + +
- )) - )} -
+
+ )) + )}
@@ -341,7 +340,7 @@ export function McpConfigModal({
{labels.mcpHint} -
+
{labels.envParamsHint} -
+
+ ); } @@ -444,24 +442,27 @@ export function TemplateManagerModal({ }: TemplateManagerModalProps) { if (!open) return null; + const activeTemplateCount = templateTab === 'agent' ? templateAgentCount : templateTopicCount; + const activeTemplateLabel = templateTab === 'agent' ? labels.templateTabAgent : labels.templateTabTopic; + const activeTemplateText = templateTab === 'agent' ? templateAgentText : templateTopicText; + const activeTemplatePlaceholder = templateTab === 'agent' ? '{"agents_md":"..."}' : '{"presets":[...]}'; + const handleTemplateTextChange = templateTab === 'agent' ? onTemplateAgentTextChange : onTemplateTopicTextChange; + return (
- {templateTab === 'agent' - ? `${labels.templateTabAgent} (${templateAgentCount})` - : `${labels.templateTabTopic} (${templateTopicCount})`} + {`${activeTemplateLabel} (${activeTemplateCount})`}
-
+
)} > -
- +
-
- {templateTab === 'agent' ? ( +
+