From 7f2b6a6ebf3b9c664490b888e76d32d5a26886cc Mon Sep 17 00:00:00 2001 From: GCWing Date: Tue, 24 Mar 2026 19:51:30 +0800 Subject: [PATCH] feat(web-ui): rich export toast, drop placeholder feedback, polish UI - Notifications: optional messageNode for clickable path + reveal in explorer on export success - Model round: remove like/dislike/report stub buttons - About dialog medium size; boost menu solid background; notification badge sizing --- .../components/AboutDialog/AboutDialog.tsx | 2 +- .../src/flow_chat/components/ChatInput.scss | 6 +-- .../components/modern/ExportImageButton.tsx | 34 ++++++++++++++++- .../components/modern/ModelRoundItem.scss | 24 ------------ .../components/modern/ModelRoundItem.tsx | 38 +------------------ src/web-ui/src/locales/en-US/flow-chat.json | 1 + src/web-ui/src/locales/zh-CN/flow-chat.json | 1 + .../components/NotificationCenter.scss | 8 ++-- .../components/NotificationCenter.tsx | 4 +- .../components/NotificationItem.scss | 18 +++++++++ .../components/NotificationItem.tsx | 4 +- .../services/NotificationService.ts | 1 + .../shared/notification-system/types/index.ts | 7 ++++ 13 files changed, 72 insertions(+), 76 deletions(-) diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx index e4c95f23..05e78333 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx @@ -51,7 +51,7 @@ export const AboutDialog: React.FC = ({ onClose={onClose} title={t('header.about')} showCloseButton={true} - size="small" + size="medium" >
{/* Hero section - product info */} diff --git a/src/web-ui/src/flow_chat/components/ChatInput.scss b/src/web-ui/src/flow_chat/components/ChatInput.scss index 69140316..e08f1865 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.scss +++ b/src/web-ui/src/flow_chat/components/ChatInput.scss @@ -758,11 +758,7 @@ max-height: 280px; display: flex; flex-direction: column; - background: linear-gradient( - 135deg, - var(--color-bg-elevated) 0%, - var(--color-bg-tertiary) 100% - ); + background: var(--color-bg-elevated); border: 1px solid var(--border-subtle); border-radius: 8px; box-shadow: diff --git a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx index 356a2055..1df9ea94 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx @@ -13,6 +13,7 @@ import { FlowToolCard } from '../FlowToolCard'; import { Tooltip } from '@/component-library'; import type { DialogTurn, FlowTextItem, FlowToolItem } from '../../types/flow-chat'; import { i18nService } from '@/infrastructure/i18n'; +import { workspaceAPI } from '@/infrastructure/api'; import { createLogger } from '@/shared/utils/logger'; import { downloadDir, join } from '@tauri-apps/api/path'; import { writeFile } from '@tauri-apps/plugin-fs'; @@ -256,7 +257,38 @@ export const ExportImageButton: React.FC = ({ const arrayBuffer = await blob.arrayBuffer(); await writeFile(filePath, new Uint8Array(arrayBuffer)); - notificationService.success(i18nService.t('flow-chat:exportImage.exportSuccess', { filePath })); + const plainSuccessMessage = i18nService.t('flow-chat:exportImage.exportSuccess', { filePath }); + const successPrefix = i18nService.t('flow-chat:exportImage.exportSuccessPrefix'); + + const revealExportedFile = async () => { + if (typeof window === 'undefined' || !('__TAURI__' in window)) { + return; + } + try { + await workspaceAPI.revealInExplorer(filePath); + } catch (error) { + log.error('Failed to reveal export path in file manager', { filePath, error }); + } + }; + + notificationService.success(plainSuccessMessage, { + messageNode: ( + <> + {successPrefix} + + + ), + }); } catch (error) { log.error('Export failed', error); notificationService.error(i18nService.t('flow-chat:exportImage.exportFailed')); diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss index b6cc6a93..436c1465 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss @@ -44,14 +44,6 @@ margin-top: 0.5rem; } -.model-round-item__feedback-group { - display: flex; - align-items: center; - gap: 0.25rem; - padding-left: 0.5rem; - border-left: 1px solid rgba(255, 255, 255, 0.1); -} - .model-round-item__action-btn { display: flex; align-items: center; @@ -112,22 +104,6 @@ } } -// Feedback buttons -.model-round-item__feedback-btn { - &:hover:has(svg[data-lucide="thumbs-up"]) { - color: #22c55e; - } - - &:hover:has(svg[data-lucide="thumbs-down"]) { - color: #ef4444; - } - - &:hover:has(svg[data-lucide="alert-triangle"]) { - color: #f59e0b; - } -} - - @keyframes fadeIn { from { opacity: 0; diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx index 8dec096e..52df920f 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Copy, Check, ThumbsUp, ThumbsDown, AlertTriangle } from 'lucide-react'; +import { Copy, Check } from 'lucide-react'; import type { ModelRound, FlowItem, FlowTextItem, FlowToolItem, FlowThinkingItem } from '../../types/flow-chat'; import { FlowTextBlock } from '../FlowTextBlock'; import { FlowToolCard } from '../FlowToolCard'; @@ -17,7 +17,6 @@ import { isCollapsibleTool } from '../../tool-cards'; import { useFlowChatContext } from './FlowChatContext'; import { FlowChatStore } from '../../store/FlowChatStore'; import { taskCollapseStateManager } from '../../store/TaskCollapseStateManager'; -import { notificationService } from '../../../shared/notification-system/services/NotificationService'; import { ExportImageButton } from './ExportImageButton'; import { Tooltip } from '@/component-library'; import { createLogger } from '@/shared/utils/logger'; @@ -62,12 +61,6 @@ export const ModelRoundItem = React.memo( }; }, [copied]); - const handleFeedback = useCallback((_type: 'like' | 'dislike' | 'report') => { - notificationService.info(t('modelRound.feedbackThanks'), { - title: t('modelRound.feedbackDevVersion') - }); - }, [t]); - // Keep insertion order; do not sort by timestamp. // Subagent ordering is controlled by insertModelRoundItemAfterTool. // FlowChatStore uses immutable updates, so rely on round.items reference. @@ -354,35 +347,6 @@ export const ModelRoundItem = React.memo( - -
- - - - - - - - - - - -
)} diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 87445c74..0f782afb 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -339,6 +339,7 @@ "generateFailed": "Failed to generate image", "fileNamePrefix": "BitFun-Chat", "exportSuccess": "Image exported: {{filePath}}", + "exportSuccessPrefix": "Image exported: ", "exportFailed": "Export failed, please try again", "exporting": "Exporting...", "exportToImage": "Export as Image" diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 14d342e7..974a9317 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -339,6 +339,7 @@ "generateFailed": "生成图片失败", "fileNamePrefix": "BitFun对话", "exportSuccess": "图片已导出:{{filePath}}", + "exportSuccessPrefix": "图片已导出:", "exportFailed": "导出图片失败,请重试", "exporting": "正在导出...", "exportToImage": "导出为图片" diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss index f88531a7..50e459e0 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss @@ -347,11 +347,11 @@ &__item-badge { flex-shrink: 0; - width: 6px; - height: 6px; - background: var(--color-error); + width: 10px; + height: 10px; + background: var(--color-error); border-radius: 50%; - border: 1px solid $color-bg-primary; + border: none; margin-left: auto; margin-right: $size-gap-1; align-self: center; diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx index 48a7af46..8373aa15 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx @@ -201,7 +201,7 @@ export const NotificationCenter: React.FC = () => { )}
- {isProgress && notification.progressText ? notification.progressText : notification.message} + {isProgress && notification.progressText ? notification.progressText : (notification.messageNode ?? notification.message)}
{isProgress && (() => { @@ -280,7 +280,7 @@ export const NotificationCenter: React.FC = () => { })()}
- {(isProgress && notification.progressText) ? notification.progressText : notification.message} + {(isProgress && notification.progressText) ? notification.progressText : (notification.messageNode ?? notification.message)}
{isProgress && (() => { diff --git a/src/web-ui/src/shared/notification-system/components/NotificationItem.scss b/src/web-ui/src/shared/notification-system/components/NotificationItem.scss index dc3a661b..e65b5e6a 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationItem.scss +++ b/src/web-ui/src/shared/notification-system/components/NotificationItem.scss @@ -82,6 +82,24 @@ word-break: break-word; } + &__path-link { + display: inline; + margin: 0; + padding: 0; + border: none; + background: none; + font: inherit; + color: var(--color-primary); + text-decoration: underline; + text-align: left; + cursor: pointer; + word-break: break-all; + + &:hover { + color: var(--color-accent, var(--color-primary)); + } + } + &__actions { display: flex; diff --git a/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx b/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx index d3cf1be3..4f9df831 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx +++ b/src/web-ui/src/shared/notification-system/components/NotificationItem.tsx @@ -12,7 +12,7 @@ export interface NotificationItemProps { } export const NotificationItem: React.FC = ({ notification }) => { - const { id, type, title, message, closable, actions } = notification; + const { id, type, title, message, messageNode, closable, actions } = notification; const { t } = useI18n('common'); @@ -54,7 +54,7 @@ export const NotificationItem: React.FC = ({ notification
{title}
-
{message}
+
{messageNode ?? message}
{actions && actions.length > 0 && ( diff --git a/src/web-ui/src/shared/notification-system/services/NotificationService.ts b/src/web-ui/src/shared/notification-system/services/NotificationService.ts index b128e341..472e788e 100644 --- a/src/web-ui/src/shared/notification-system/services/NotificationService.ts +++ b/src/web-ui/src/shared/notification-system/services/NotificationService.ts @@ -58,6 +58,7 @@ class NotificationService { variant: 'toast', title: options?.title || this.getDefaultTitle(type), message, + messageNode: options?.messageNode, timestamp: Date.now(), duration: options?.duration ?? state.config.defaultDuration, closable: options?.closable ?? true, diff --git a/src/web-ui/src/shared/notification-system/types/index.ts b/src/web-ui/src/shared/notification-system/types/index.ts index c5079b59..44b834d9 100644 --- a/src/web-ui/src/shared/notification-system/types/index.ts +++ b/src/web-ui/src/shared/notification-system/types/index.ts @@ -3,6 +3,8 @@ * * Shared by the notification store, service, and UI components. */ +import type { ReactNode } from 'react'; + export type NotificationType = 'success' | 'error' | 'warning' | 'info'; @@ -43,6 +45,9 @@ export interface Notification { title: string; message: string; + + /** When set, toast/history render this instead of plain `message` (keep `message` for search/plain fallback). */ + messageNode?: ReactNode; timestamp: number; @@ -103,6 +108,8 @@ export interface ToastOptions { closable?: boolean; actions?: NotificationAction[]; + + messageNode?: ReactNode; metadata?: Record; }