From 50d0e65b5df17410c44488ea109f2736b4302406 Mon Sep 17 00:00:00 2001 From: wsp Date: Tue, 8 Sep 2026 19:43:54 +0800 Subject: [PATCH 1/2] feat(flow-chat): Add image export modes Offer compact images and images with thinking from the export menu. Keep thinking headers in compact exports and show the full reasoning in the detailed mode, with localized labels for all supported locales. Preserve the existing exploration and tool-card export behavior. --- .../components/modern/ExportImageButton.scss | 10 +- .../components/modern/ExportImageButton.tsx | 112 ++++++++++++++---- src/web-ui/src/locales/en-US/flow-chat.json | 2 + src/web-ui/src/locales/zh-CN/flow-chat.json | 2 + src/web-ui/src/locales/zh-TW/flow-chat.json | 2 + 5 files changed, 101 insertions(+), 27 deletions(-) diff --git a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss index a58443971f..d1c0f9e711 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss @@ -5,6 +5,11 @@ overflow: visible; } +.export-image-menu { + position: fixed; + z-index: var(--openbitfun-layer-popover); +} + // Export content container .export-content { /* Width is set dynamically in JS to match the live chat pane width. @@ -147,7 +152,9 @@ &__thinking-item { margin-bottom: 8px; - + } + + &__thinking-item--expanded { // Show full thinking content without scrollbars. .thinking-content { max-height: none !important; @@ -206,4 +213,3 @@ white-space: nowrap; } } - 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 e0cc7fd009..a27a025f2b 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx @@ -4,15 +4,18 @@ * Uses modern-screenshot (fork of html-to-image with better CSS var / font / CORS handling). */ -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { createRoot } from 'react-dom/client'; import { FlowChatStore } from '../../store/FlowChatStore'; import { notificationService } from '@/shared/notification-system'; import { FlowTextBlock } from '../FlowTextBlock'; import { FlowToolCard } from '../FlowToolCard'; -import { Icon, Tooltip } from '@openbitfun/ui'; +import { Icon, Menu, MenuItem, Tooltip } from '@openbitfun/ui'; +import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost'; +import { useAnchoredPopoverPosition } from '@/shared/utils/useAnchoredPopoverPosition'; import type { DialogTurn, FlowTextItem, FlowToolItem, FlowThinkingItem } from '../../types/flow-chat'; -import { i18nService } from '@/infrastructure/i18n'; +import { i18nService, useI18n } from '@/infrastructure/i18n'; import { workspaceAPI } from '@/infrastructure/api'; import { createLogger } from '@/shared/utils/logger'; import { getBuiltinAppearanceThemeToken } from '@/infrastructure/appearance/builtins/catalog'; @@ -74,12 +77,13 @@ interface ExportImageButtonProps { // Exported content renderer. interface ExportContentProps { dialogTurn: DialogTurn; + expandThinking: boolean; } // Marker class on the logo placeholder so we can locate it for canvas compositing. const LOGO_PLACEHOLDER_CLASS = 'export-content__logo-placeholder'; -const ExportContent: React.FC = ({ dialogTurn }) => { +const ExportContent: React.FC = ({ dialogTurn, expandThinking }) => { return (
@@ -133,13 +137,14 @@ const ExportContent: React.FC = ({ dialogTurn }) => { } else if (item.type === 'thinking') { const thinkingItem = item as FlowThinkingItem; return ( -
+
); @@ -171,7 +176,38 @@ export const ExportImageButton: React.FC = ({ turnId, className = '' }) => { + const { t } = useI18n('flow-chat'); const [isExporting, setIsExporting] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const buttonRef = useRef(null); + const menuRef = useRef(null); + const menuLayout = useAnchoredPopoverPosition({ + open: isMenuOpen, + anchorRef: buttonRef, + popoverRef: menuRef, + preferredPlacement: 'top', + alignment: 'end', + gap: 4, + }); + + useEffect(() => { + if (!isMenuOpen) return; + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if (!buttonRef.current?.contains(target) && !menuRef.current?.contains(target)) { + setIsMenuOpen(false); + } + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsMenuOpen(false); + }; + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isMenuOpen]); // Ref guard to prevent double-invocation while state update is pending. const isExportingRef = useRef(false); @@ -187,8 +223,9 @@ export const ExportImageButton: React.FC = ({ return null; }, [turnId]); - const handleExport = useCallback(async () => { + const handleExport = useCallback(async (expandThinking: boolean) => { if (isExportingRef.current) return; + setIsMenuOpen(false); isExportingRef.current = true; setIsExporting(true); @@ -283,7 +320,7 @@ export const ExportImageButton: React.FC = ({ // Render export content with React. root = createRoot(wrapper); - root.render(); + root.render(); // Adaptive render wait: base time + per-round allowance, capped. const roundCount = dialogTurn.modelRounds?.length ?? 1; @@ -545,23 +582,48 @@ export const ExportImageButton: React.FC = ({ }, [getDialogTurn]); return ( - - - + <> + + + + {isMenuOpen && createPortal( + + void handleExport(false)}> + {t('exportImage.collapsedThinking')} + + void handleExport(true)}> + {t('exportImage.expandedThinking')} + + , + getAppearanceOverlayHost(), + )} + ); }; 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 19caa62686..6200245a52 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -1494,6 +1494,8 @@ } }, "exportImage": { + "collapsedThinking": "Compact image", + "expandedThinking": "Image with thinking", "subtitle": "AI Programming Assistant · Chat Record", "poweredBy": "Powered by", "aiAssistant": "AI Programming Assistant", 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 ebbb9d1754..a105baeade 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -1494,6 +1494,8 @@ } }, "exportImage": { + "collapsedThinking": "简洁长图", + "expandedThinking": "含思考长图", "subtitle": "AI 编程助手 · 对话记录", "poweredBy": "Powered by", "aiAssistant": "AI 编程助手", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index b5610742d2..3135d804a8 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -1494,6 +1494,8 @@ } }, "exportImage": { + "collapsedThinking": "精簡長圖", + "expandedThinking": "含思考長圖", "subtitle": "AI 編程助手 · 對話記錄", "poweredBy": "Powered by", "aiAssistant": "AI 編程助手", From 10b270cba35742318c7b8e1831f8e920839cec82 Mon Sep 17 00:00:00 2001 From: wsp Date: Tue, 8 Sep 2026 19:44:09 +0800 Subject: [PATCH 2/2] fix(desktop): Reveal Windows files via Shell API Use the existing opener integration to select files in Explorer. Passing /select and a path as one argument quotes the whole switch when filenames contain spaces, which can open the wrong location. Preserve directory opening and the macOS and Linux implementations. --- src/apps/desktop/src/api/commands.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apps/desktop/src/api/commands.rs b/src/apps/desktop/src/api/commands.rs index f8f995d22f..035825a611 100644 --- a/src/apps/desktop/src/api/commands.rs +++ b/src/apps/desktop/src/api/commands.rs @@ -4352,11 +4352,11 @@ pub(crate) fn reveal_local_path_in_explorer( .spawn() .map_err(|e| format!("Failed to open explorer: {}", e))?; } else { - let normalized_path = path_str.replace("/", "\\"); - openbitfun_core::util::process_manager::create_command("explorer") - .arg(format!("/select,{}", normalized_path)) - .spawn() - .map_err(|e| format!("Failed to open explorer: {}", e))?; + // Explorer does not use standard argv quoting for /select: Command + // quotes the entire switch + path when a filename contains spaces. + // Use Shell item IDs instead so the path is never a command line. + tauri_plugin_opener::reveal_item_in_dir(path) + .map_err(|e| format!("Failed to reveal file in explorer: {}", e))?; } }