diff --git a/frontend/src/hooks/useBotsSync.ts b/frontend/src/hooks/useBotsSync.ts index 3bb5429..98c01c0 100644 --- a/frontend/src/hooks/useBotsSync.ts +++ b/frontend/src/hooks/useBotsSync.ts @@ -29,6 +29,20 @@ function normalizeChannelName(raw: unknown): string { return channel; } +function isLikelyEchoOfUserInput(progressText: string, userText: string): boolean { + const progress = normalizeAssistantMessageText(progressText).replace(/\s+/g, ' ').trim().toLowerCase(); + const user = normalizeUserMessageText(userText).replace(/\s+/g, ' ').trim().toLowerCase(); + if (!progress || !user) return false; + if (progress === user) return true; + if (user.length < 8) return false; + const hasProcessingPrefix = + /processing message|message from|received message|收到消息|处理消息|用户输入|command/i.test(progress); + if (progress.includes(user) && (hasProcessingPrefix || progress.length <= user.length + 40)) { + return true; + } + return false; +} + export function useBotsSync() { const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore(); const socketsRef = useRef>({}); @@ -157,8 +171,8 @@ export function useBotsSync() { ts: Date.now(), channel: sourceChannel || undefined, }); - if (isDashboardChannel && fullMessage && (normalizedState === 'THINKING' || normalizedState === 'TOOL_CALL')) { - const chatText = normalizedState === 'TOOL_CALL' ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullMessage}` : fullMessage; + if (isDashboardChannel && fullMessage && normalizedState === 'TOOL_CALL') { + const chatText = `${isZh ? '工具调用' : 'Tool Call'}\n${fullMessage}`; const now = Date.now(); const prev = lastProgressRef.current[bot.id]; if (!prev || prev.text !== chatText || now - prev.ts > 1200) { @@ -198,6 +212,10 @@ export function useBotsSync() { updateBotState(bot.id, state, fullProgress); addBotEvent(bot.id, { state, text: fullProgress || t.progress, ts: Date.now(), channel: sourceChannel || undefined }); if (isDashboardChannel) { + const lastUserText = lastUserEchoRef.current[bot.id]?.text || ''; + if (!isTool && isLikelyEchoOfUserInput(fullProgress, lastUserText)) { + return; + } const chatText = isTool ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullProgress}` : fullProgress; const now = Date.now(); const prev = lastProgressRef.current[bot.id]; diff --git a/frontend/src/modules/dashboard/BotDashboardModule.css b/frontend/src/modules/dashboard/BotDashboardModule.css index 409dbad..945fd79 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.css +++ b/frontend/src/modules/dashboard/BotDashboardModule.css @@ -366,6 +366,29 @@ color: var(--muted); } +.ops-chat-meta-right { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.ops-chat-expand-icon-btn { + border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%); + background: color-mix(in oklab, var(--panel) 76%, var(--brand-soft) 24%); + color: var(--text); + border-radius: 999px; + font-size: 12px; + font-weight: 700; + line-height: 1; + width: 20px; + height: 20px; + padding: 0; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + .ops-chat-text { white-space: pre-wrap; word-break: break-word; @@ -392,20 +415,6 @@ background: linear-gradient(to bottom, transparent, color-mix(in oklab, var(--panel-soft) 88%, var(--panel) 12%)); } -.ops-chat-more-btn { - position: relative; - z-index: 1; - margin-top: 8px; - border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%); - background: color-mix(in oklab, var(--panel) 72%, var(--brand-soft) 28%); - color: var(--text); - border-radius: 999px; - font-size: 12px; - font-weight: 700; - padding: 4px 10px; - cursor: pointer; -} - .ops-chat-text > *:first-child { margin-top: 0; } @@ -1074,24 +1083,42 @@ font-weight: 700; } -.ops-runtime-action { +.ops-runtime-action-inline { min-width: 0; - display: grid; + display: flex; + align-items: flex-start; gap: 6px; } .ops-runtime-action-text { - display: block; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; line-height: 1.54; - max-height: 82px; - overflow: auto; + overflow: hidden; + flex: 1; + min-width: 0; } -.ops-runtime-action-text.expanded { - max-height: 180px; +.ops-runtime-expand-btn { + border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%); + background: color-mix(in oklab, var(--panel) 76%, var(--brand-soft) 24%); + color: var(--text); + border-radius: 999px; + font-size: 14px; + font-weight: 700; + line-height: 1; + width: 22px; + height: 22px; + padding: 0; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; } .ops-preview { diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index f80ab02..91292a5 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -249,7 +249,8 @@ function decorateWorkspacePathsForMarkdown(text: string) { /\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\s*\n?\s*\((https:\/\/workspace\.local\/open\/[^)\s]+)\)/g, '[$1]($2)', ); - const workspacePathPattern = /\/root\/\.nanobot\/workspace\/[^\s<>"'`)\]},。!?;:、]+/g; + const workspacePathPattern = + /\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b/gi; return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => { const normalized = normalizeDashboardAttachmentPath(fullPath); if (!normalized) return fullPath; @@ -391,7 +392,7 @@ export function BotDashboardModule({ const [compactPanelTab, setCompactPanelTab] = useState('chat'); const [isCompactMobile, setIsCompactMobile] = useState(false); const [expandedProgressByKey, setExpandedProgressByKey] = useState>({}); - const [runtimeActionExpanded, setRuntimeActionExpanded] = useState(false); + const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false); const runtimeMenuRef = useRef(null); const openWorkspacePathFromChat = (path: string) => { const normalized = String(path || '').trim(); @@ -406,7 +407,7 @@ export function BotDashboardModule({ const source = String(text || ''); if (!source) return [source]; const pattern = - /\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\]},。!?;:、]+|https:\/\/workspace\.local\/open\/[^\s)]+/g; + /\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi; const nodes: ReactNode[] = []; let lastIndex = 0; let matchIndex = 0; @@ -624,7 +625,7 @@ export function BotDashboardModule({ const summary = String(runtimeActionSummary || '').trim(); return Boolean(full && full !== '-' && summary && full !== summary); }, [runtimeAction, runtimeActionSummary]); - const runtimeActionDisplay = runtimeActionExpanded || !runtimeActionHasMore ? runtimeAction : runtimeActionSummary; + const runtimeActionDisplay = runtimeActionHasMore ? runtimeActionSummary : runtimeAction; const shouldCollapseProgress = (text: string) => { const normalized = String(text || '').trim(); @@ -656,7 +657,24 @@ export function BotDashboardModule({
{item.role === 'user' ? t.you : 'Nanobot'} - {formatClock(item.ts)} +
+ {formatClock(item.ts)} + {collapsible ? ( + + ) : null} +
{item.text ? ( @@ -668,19 +686,6 @@ export function BotDashboardModule({ ) ) : null} - {collapsible ? ( - - ) : null} {(item.attachments || []).length > 0 ? (
{(item.attachments || []).map((rawPath) => { @@ -752,7 +757,7 @@ export function BotDashboardModule({ useEffect(() => { setExpandedProgressByKey({}); - setRuntimeActionExpanded(false); + setShowRuntimeActionModal(false); }, [selectedBotId]); useEffect(() => { @@ -1951,14 +1956,16 @@ export function BotDashboardModule({
{t.current}{displayState}
{t.lastAction} -
- {runtimeActionDisplay} +
+ {runtimeActionDisplay} {runtimeActionHasMore ? ( ) : null}
@@ -2468,6 +2475,23 @@ export function BotDashboardModule({
)} + {showRuntimeActionModal && ( +
setShowRuntimeActionModal(false)}> +
e.stopPropagation()}> +
+

{t.lastAction}

+
+
+
{runtimeAction}
+
+
+ + +
+
+
+ )} + {workspacePreview && (
setWorkspacePreview(null)}>
e.stopPropagation()}>