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}
-
+