Browse Source

输入模式修改

feature/composer
TianYun 3 weeks ago
parent
commit
d977a25ec7
  1. 43
      src/components/AppSettingsModal.tsx
  2. 72
      src/components/WorkflowCanvas.tsx
  3. 1
      src/components/__tests__/Header.test.tsx
  4. 50
      src/components/__tests__/WorkflowCanvas.test.tsx
  5. 32
      src/i18n/index.tsx
  6. 3
      src/store/__tests__/workflowStore.integration.test.ts
  7. 3
      src/store/utils/__tests__/localStorage.test.ts
  8. 3
      src/types/canvas.ts

43
src/components/AppSettingsModal.tsx

@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { Button, Select, Slider, Switch } from "antd";
import { useWorkflowStore } from "@/store/workflowStore";
import { NodeDefaultsConfig, LLMProvider, LLMModelType } from "@/types";
import { CanvasNavigationSettings } from "@/types/canvas";
import { CanvasInputMode, CanvasNavigationSettings } from "@/types/canvas";
import { loadNodeDefaults, saveNodeDefaults } from "@/store/utils/localStorage";
import { useInlineParameters } from "@/hooks/useInlineParameters";
import { LANGUAGE_OPTIONS, useI18n } from "@/i18n";
@ -366,11 +366,38 @@ export function AppSettingsModal({ isOpen, onClose }: AppSettingsModalProps) {
{activeTab === "canvas" && (
<div className="space-y-3">
<p className="text-xs text-[var(--text-muted)]">{t("project.canvasHelp")}</p>
<div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-2)] p-3 shadow-[var(--shadow-panel)]">
<div className="mb-3 flex items-center justify-between gap-4">
<span className="text-sm font-medium text-[var(--text-primary)]">{t("project.inputMode")}</span>
<Select
value={localCanvasSettings.inputMode}
onChange={(value) => setLocalCanvasSettings((settings) => ({
...settings,
inputMode: value as CanvasInputMode,
}))}
className="w-36"
options={[
{ value: "mouse", label: t("project.inputModeMouseLabel") },
{ value: "trackpad", label: t("project.inputModeTrackpadLabel") },
]}
/>
</div>
<p className="text-xs text-[var(--text-muted)]">
{localCanvasSettings.inputMode === "trackpad"
? t("project.inputModeTrackpad")
: t("project.inputModeMouse")}
</p>
</div>
<div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-2)] p-3 shadow-[var(--shadow-panel)]">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium text-[var(--text-primary)]">{t("project.panMode")}</span>
<p className="text-right text-xs text-[var(--text-muted)]">{t("project.panAlways")}</p>
<p className="text-right text-xs text-[var(--text-muted)]">
{localCanvasSettings.inputMode === "trackpad"
? t("project.panTrackpad")
: t("project.panAlways")}
</p>
</div>
</div>
</div>
@ -379,7 +406,11 @@ export function AppSettingsModal({ isOpen, onClose }: AppSettingsModalProps) {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium text-[var(--text-primary)]">{t("project.zoomMode")}</span>
<p className="text-right text-xs text-[var(--text-muted)]">{t("project.zoomScroll")}</p>
<p className="text-right text-xs text-[var(--text-muted)]">
{localCanvasSettings.inputMode === "trackpad"
? t("project.zoomTrackpad")
: t("project.zoomScroll")}
</p>
</div>
</div>
</div>
@ -388,7 +419,11 @@ export function AppSettingsModal({ isOpen, onClose }: AppSettingsModalProps) {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium text-[var(--text-primary)]">{t("project.selectionMode")}</span>
<p className="text-right text-xs text-[var(--text-muted)]">{t("project.selectRightSpace")}</p>
<p className="text-right text-xs text-[var(--text-muted)]">
{localCanvasSettings.inputMode === "trackpad"
? t("project.selectTrackpad")
: t("project.selectRightSpace")}
</p>
</div>
</div>
</div>

72
src/components/WorkflowCanvas.tsx

@ -127,6 +127,12 @@ type FlowRect = {
height: number;
};
type WebKitGestureEvent = Event & {
scale: number;
clientX?: number;
clientY?: number;
};
const CANVAS_LOCAL_INTERACTION_SELECTOR = [
".nodrag",
".nopan",
@ -623,6 +629,7 @@ export function WorkflowCanvas() {
const boxSelectionEndPointRef = useRef<ScreenPoint | null>(null);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const tutorialViewportSet = useRef(false);
const gestureZoomStartRef = useRef<{ zoom: number } | null>(null);
const [boxSelectionRect, setBoxSelectionRect] = useState<FlowRect | null>(null);
const onNodesChange = useCallback((changes: NodeChange<WorkflowNode>[]) => {
@ -761,7 +768,9 @@ export function WorkflowCanvas() {
return unsubscribe;
}, []);
const canvasPanDisabled = tutorialActive || isModalOpen;
const canvasInputMode = canvasNavigationSettings.inputMode;
const isTrackpadInputMode = canvasInputMode === "trackpad";
const canvasPanDisabled = tutorialActive || isModalOpen || isTrackpadInputMode;
const canvasPanOnDrag = useMemo(
() => getCanvasPanOnDrag(canvasPanDisabled),
[canvasPanDisabled]
@ -2155,15 +2164,68 @@ export function WorkflowCanvas() {
event.preventDefault();
const viewport = getViewport();
const factor = event.deltaY < 0 ? WHEEL_ZOOM_FACTOR : 1 / WHEEL_ZOOM_FACTOR;
zoomAtScreenPoint({ x: event.clientX, y: event.clientY }, viewport.zoom * factor);
const shouldZoom = !isTrackpadInputMode || event.ctrlKey || event.metaKey;
if (shouldZoom) {
const factor = event.deltaY < 0 ? WHEEL_ZOOM_FACTOR : 1 / WHEEL_ZOOM_FACTOR;
zoomAtScreenPoint({ x: event.clientX, y: event.clientY }, viewport.zoom * factor);
return;
}
setViewport({
x: viewport.x - event.deltaX,
y: viewport.y - event.deltaY,
zoom: viewport.zoom,
});
};
wrapper.addEventListener('wheel', handleWheelNonPassive, { passive: false });
return () => {
wrapper.removeEventListener('wheel', handleWheelNonPassive);
};
}, [getViewport, isModalOpen, zoomAtScreenPoint]);
}, [getViewport, isModalOpen, isTrackpadInputMode, setViewport, zoomAtScreenPoint]);
useEffect(() => {
if (!isTrackpadInputMode) return;
const wrapper = reactFlowWrapper.current;
if (!wrapper) return;
const getGesturePoint = (event: WebKitGestureEvent) => {
if (typeof event.clientX === "number" && typeof event.clientY === "number") {
return { x: event.clientX, y: event.clientY };
}
return getCanvasScreenCenter();
};
const handleGestureStart = (event: Event) => {
event.preventDefault();
gestureZoomStartRef.current = { zoom: getViewport().zoom };
};
const handleGestureChange = (event: Event) => {
event.preventDefault();
const gestureEvent = event as WebKitGestureEvent;
const start = gestureZoomStartRef.current ?? { zoom: getViewport().zoom };
gestureZoomStartRef.current = start;
zoomAtScreenPoint(getGesturePoint(gestureEvent), start.zoom * gestureEvent.scale);
};
const handleGestureEnd = (event: Event) => {
event.preventDefault();
gestureZoomStartRef.current = null;
};
wrapper.addEventListener("gesturestart", handleGestureStart, { passive: false });
wrapper.addEventListener("gesturechange", handleGestureChange, { passive: false });
wrapper.addEventListener("gestureend", handleGestureEnd, { passive: false });
return () => {
wrapper.removeEventListener("gesturestart", handleGestureStart);
wrapper.removeEventListener("gesturechange", handleGestureChange);
wrapper.removeEventListener("gestureend", handleGestureEnd);
gestureZoomStartRef.current = null;
};
}, [getCanvasScreenCenter, getViewport, isTrackpadInputMode, zoomAtScreenPoint]);
// Keyboard shortcuts for copy/paste and stacking selected nodes
const handleKeyDown = useCallback((event: KeyboardEvent) => {
@ -3004,7 +3066,7 @@ export function WorkflowCanvas() {
fitViewOptions={FIT_VIEW_OPTIONS}
deleteKeyCode={["Backspace", "Delete"]}
multiSelectionKeyCode="Shift"
selectionOnDrag={false}
selectionOnDrag={isTrackpadInputMode}
selectionKeyCode={["Space", "Control", "Meta"]}
panOnDrag={canvasPanOnDrag}
selectNodesOnDrag={false}

1
src/components/__tests__/Header.test.tsx

@ -64,6 +64,7 @@ const createDefaultState = (overrides = {}) => ({
markAsSaved: mockMarkAsSaved,
canvasNavigationSettings: {
theme: "dark",
inputMode: "mouse",
panMode: "space",
zoomMode: "altScroll",
selectionMode: "click",

50
src/components/__tests__/WorkflowCanvas.test.tsx

@ -169,7 +169,7 @@ const createDefaultState = (overrides = {}) => ({
getNodesWithComments: vi.fn(() => []),
getConnectedInputs: vi.fn(() => ({ images: [], videos: [], audio: [], text: null })),
markCommentViewed: vi.fn(),
canvasNavigationSettings: { theme: "dark", panMode: "space", zoomMode: "altScroll", selectionMode: "click", assistantEnabled: false },
canvasNavigationSettings: { theme: "dark", inputMode: "mouse", panMode: "space", zoomMode: "altScroll", selectionMode: "click", assistantEnabled: false },
updateCanvasNavigationSettings: mockUpdateCanvasNavigationSettings,
dimmedNodeIds: new Set<string>(),
skippedNodeIds: new Set<string>(),
@ -279,6 +279,7 @@ describe("WorkflowCanvas", () => {
return selector(createDefaultState({
canvasNavigationSettings: {
theme: "dark",
inputMode: "mouse",
panMode: "space",
zoomMode: "altScroll",
selectionMode: "click",
@ -301,6 +302,7 @@ describe("WorkflowCanvas", () => {
const enabledSettings = {
panMode: "space" as const,
theme: "dark" as const,
inputMode: "mouse" as const,
zoomMode: "altScroll" as const,
selectionMode: "click" as const,
assistantEnabled: true,
@ -1124,6 +1126,52 @@ describe("WorkflowCanvas", () => {
expect(document.querySelector(".react-flow")).toBeInTheDocument();
});
it("should zoom on wheel in mouse input mode", () => {
render(
<TestWrapper>
<WorkflowCanvas />
</TestWrapper>
);
const canvas = document.querySelector(".canvas-default-cursor") as HTMLElement;
fireEvent.wheel(canvas, { deltaX: 0, deltaY: -100, clientX: 50, clientY: 60 });
const [viewport] = mockSetViewport.mock.calls[0];
expect(viewport.x).toBeCloseTo(-7.5);
expect(viewport.y).toBeCloseTo(-9);
expect(viewport.zoom).toBeCloseTo(1.15);
});
it("should pan on wheel in trackpad input mode", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
canvasNavigationSettings: {
theme: "dark",
inputMode: "trackpad",
panMode: "space",
zoomMode: "altScroll",
selectionMode: "click",
assistantEnabled: false,
},
}));
});
render(
<TestWrapper>
<WorkflowCanvas />
</TestWrapper>
);
const canvas = document.querySelector(".canvas-default-cursor") as HTMLElement;
fireEvent.wheel(canvas, { deltaX: 12, deltaY: 30, clientX: 50, clientY: 60 });
expect(mockSetViewport).toHaveBeenCalledWith({
x: -12,
y: -30,
zoom: 1,
});
});
it("should use editable as default edge type", () => {
render(
<TestWrapper>

32
src/i18n/index.tsx

@ -838,6 +838,11 @@ const en = {
"project.executionHelp": "Maximum number of nodes to execute in parallel during workflow execution. Higher values may improve speed but increase API rate limit risk.",
"project.defaultsHelp": "These defaults are applied when creating nodes via keyboard shortcuts (Shift+G, Shift+L, etc).",
"project.canvasHelp": "Canvas navigation and selection behavior.",
"project.inputMode": "Input Mode",
"project.inputModeMouse": "Mouse mode keeps left drag for panning and scroll for zoom.",
"project.inputModeTrackpad": "Trackpad mode uses one-finger drag for box select, two-finger scroll for panning, and pinch for zoom.",
"project.inputModeMouseLabel": "Mouse",
"project.inputModeTrackpadLabel": "Trackpad",
"project.theme": "Theme",
"project.themeDark": "Black canvas with dark panels",
"project.themeLight": "Light canvas with neutral panels",
@ -849,6 +854,7 @@ const en = {
"project.panSpace": "Left drag pans; Space/Ctrl + left drag box-selects nodes",
"project.panMiddle": "Left drag pans the canvas",
"project.panAlways": "Left drag pans the canvas",
"project.panTrackpad": "Two-finger scroll pans the canvas",
"project.panSpaceLabel": "Left Pan + Box Select",
"project.panMiddleLabel": "Left Pan",
"project.panAlwaysLabel": "Always On",
@ -856,11 +862,13 @@ const en = {
"project.zoomAlt": "Hold Alt and scroll to zoom",
"project.zoomCtrl": "Hold Ctrl/Cmd and scroll to zoom",
"project.zoomScroll": "Scroll to zoom without modifier keys",
"project.zoomTrackpad": "Pinch to zoom, or Ctrl/Cmd + scroll",
"project.zoomAltLabel": "Alt + Scroll",
"project.zoomCtrlLabel": "Ctrl + Scroll",
"project.zoomScrollLabel": "Scroll",
"project.selectionMode": "Selection",
"project.selectRightSpace": "Space + left drag or Ctrl/Cmd + left drag box-selects nodes",
"project.selectTrackpad": "One-finger drag box-selects nodes; Space/Ctrl/Cmd + drag still works",
"project.selectClick": "Click to select nodes",
"project.selectAlt": "Hold Alt and drag to select",
"project.selectShift": "Hold Shift and drag to select",
@ -1863,6 +1871,11 @@ const zhCN: Dictionary = {
"project.executionHelp": "工作流执行时最多并行运行的节点数量。数值更高可能更快,但也更容易触发 API 限流。",
"project.defaultsHelp": "这些默认值会在通过快捷键创建节点时应用(Shift+G、Shift+L 等)。",
"project.canvasHelp": "画布导航和选择操作。",
"project.inputMode": "输入模式",
"project.inputModeMouse": "鼠标模式保留左键拖拽移动画布、滚动缩放。",
"project.inputModeTrackpad": "触控板模式使用单指拖拽框选、双指滑动移动画布、捏合缩放。",
"project.inputModeMouseLabel": "鼠标",
"project.inputModeTrackpadLabel": "触控板",
"project.theme": "主题",
"project.themeDark": "黑色画布和深色面板",
"project.themeLight": "浅色画布和中性面板",
@ -1874,6 +1887,7 @@ const zhCN: Dictionary = {
"project.panSpace": "左键拖拽移动画布;Space + 左键或 Ctrl + 左键框选节点",
"project.panMiddle": "左键拖拽移动画布",
"project.panAlways": "左键拖拽移动画布",
"project.panTrackpad": "双指滑动移动画布",
"project.panSpaceLabel": "左键平移 + 框选",
"project.panMiddleLabel": "左键平移",
"project.panAlwaysLabel": "始终开启",
@ -1881,11 +1895,13 @@ const zhCN: Dictionary = {
"project.zoomAlt": "按住 Alt 并滚动缩放",
"project.zoomCtrl": "按住 Ctrl/Cmd 并滚动缩放",
"project.zoomScroll": "直接滚动缩放",
"project.zoomTrackpad": "双指捏合缩放,或 Ctrl/Cmd + 滚动",
"project.zoomAltLabel": "Alt + 滚动",
"project.zoomCtrlLabel": "Ctrl + 滚动",
"project.zoomScrollLabel": "滚动",
"project.selectionMode": "选择节点",
"project.selectRightSpace": "Space + 左键或 Ctrl + 左键框选节点",
"project.selectTrackpad": "单指拖拽框选节点;Space/Ctrl/Cmd + 拖拽仍可用",
"project.selectClick": "点击选择节点",
"project.selectAlt": "按住 Alt 并拖拽选择",
"project.selectShift": "按住 Shift 并拖拽选择",
@ -2639,6 +2655,11 @@ const zhTW: Dictionary = {
"project.executionSettings": "執行設定",
"project.maxParallel": "最大平行呼叫:{count}",
"project.canvasHelp": "設定畫布導覽和互動方式。",
"project.inputMode": "輸入模式",
"project.inputModeMouse": "滑鼠模式保留左鍵拖曳移動畫布、滾動縮放。",
"project.inputModeTrackpad": "觸控板模式使用單指拖曳框選、雙指滑動移動畫布、捏合縮放。",
"project.inputModeMouseLabel": "滑鼠",
"project.inputModeTrackpadLabel": "觸控板",
"project.theme": "主題",
"project.themeDark": "黑色畫布和深色面板",
"project.themeLight": "淺色畫布和中性面板",
@ -2647,8 +2668,11 @@ const zhTW: Dictionary = {
"project.canvasAssistant": "畫布助手",
"project.canvasAssistantShort": "助手",
"project.panMode": "平移模式",
"project.panTrackpad": "雙指滑動移動畫布",
"project.zoomMode": "縮放模式",
"project.zoomTrackpad": "雙指捏合縮放,或 Ctrl/Cmd + 滾動",
"project.selectionMode": "選擇模式",
"project.selectTrackpad": "單指拖曳框選節點;Space/Ctrl/Cmd + 拖曳仍可用",
"project.validating": "驗證中...",
"project.errorChooseLocalFolder": "請從瀏覽器選擇本機資料夾,這樣專案會儲存在目前操作者的裝置上。",
"modelSearch.search": "搜尋模型...",
@ -3383,6 +3407,11 @@ const ja: Dictionary = {
"project.executionHelp": "ワークフロー実行時に並列実行するノード数の上限です。値を上げると速くなる場合がありますが、API レート制限のリスクも上がります。",
"project.defaultsHelp": "これらの既定値はキーボードショートカット(Shift+G、Shift+L など)でノードを作成するときに適用されます。",
"project.canvasHelp": "キャンバスの移動と選択操作。",
"project.inputMode": "入力モード",
"project.inputModeMouse": "マウスモードでは左ドラッグでキャンバス移動、スクロールでズームします。",
"project.inputModeTrackpad": "トラックパッドモードでは1本指ドラッグで範囲選択、2本指スクロールでキャンバス移動、ピンチでズームします。",
"project.inputModeMouseLabel": "マウス",
"project.inputModeTrackpadLabel": "トラックパッド",
"project.theme": "テーマ",
"project.themeDark": "黒いキャンバスとダークパネル",
"project.themeLight": "明るいキャンバスとニュートラルパネル",
@ -3394,6 +3423,7 @@ const ja: Dictionary = {
"project.panSpace": "左ドラッグでキャンバス移動、Space/Ctrl + 左ドラッグで範囲選択",
"project.panMiddle": "左ドラッグでキャンバスを移動",
"project.panAlways": "左ドラッグでキャンバスを移動",
"project.panTrackpad": "2本指スクロールでキャンバスを移動",
"project.panSpaceLabel": "左パン + 範囲選択",
"project.panMiddleLabel": "左パン",
"project.panAlwaysLabel": "常時オン",
@ -3401,11 +3431,13 @@ const ja: Dictionary = {
"project.zoomAlt": "Alt を押しながらスクロールでズーム",
"project.zoomCtrl": "Ctrl/Cmd を押しながらスクロールでズーム",
"project.zoomScroll": "修飾キーなしでスクロールズーム",
"project.zoomTrackpad": "ピンチでズーム、または Ctrl/Cmd + スクロール",
"project.zoomAltLabel": "Alt + スクロール",
"project.zoomCtrlLabel": "Ctrl + スクロール",
"project.zoomScrollLabel": "スクロール",
"project.selectionMode": "選択",
"project.selectRightSpace": "Space + 左ドラッグ、または Ctrl/Cmd + 左ドラッグでノードを範囲選択",
"project.selectTrackpad": "1本指ドラッグでノードを範囲選択。Space/Ctrl/Cmd + ドラッグも引き続き使えます",
"project.selectClick": "クリックしてノードを選択",
"project.selectAlt": "Alt を押しながらドラッグして選択",
"project.selectShift": "Shift を押しながらドラッグして選択",

3
src/store/__tests__/workflowStore.integration.test.ts

@ -2804,6 +2804,7 @@ describe("workflowStore integration tests", () => {
const store = useWorkflowStore.getState();
store.updateCanvasNavigationSettings({
theme: "light",
inputMode: "trackpad",
panMode: "always",
zoomMode: "scroll",
selectionMode: "altDrag",
@ -2813,6 +2814,7 @@ describe("workflowStore integration tests", () => {
const state = useWorkflowStore.getState();
expect(state.canvasNavigationSettings).toEqual({
theme: "light",
inputMode: "trackpad",
panMode: "always",
zoomMode: "scroll",
selectionMode: "altDrag",
@ -2824,6 +2826,7 @@ describe("workflowStore integration tests", () => {
const store = useWorkflowStore.getState();
const settings = {
theme: "light" as const,
inputMode: "trackpad" as const,
panMode: "middleMouse" as const,
zoomMode: "ctrlScroll" as const,
selectionMode: "click" as const,

3
src/store/utils/__tests__/localStorage.test.ts

@ -344,6 +344,7 @@ describe("localStorage utilities", () => {
it("returns stored settings when data exists", () => {
const customSettings = {
theme: "light",
inputMode: "trackpad",
panMode: "always",
zoomMode: "scroll",
selectionMode: "altDrag",
@ -367,6 +368,7 @@ describe("localStorage utilities", () => {
expect(result).toEqual({
...legacySettings,
theme: "light",
inputMode: "mouse",
assistantEnabled: false,
});
});
@ -383,6 +385,7 @@ describe("localStorage utilities", () => {
it("persists settings to localStorage", () => {
const settings = {
theme: "light" as const,
inputMode: "trackpad" as const,
panMode: "middleMouse" as const,
zoomMode: "ctrlScroll" as const,
selectionMode: "altDrag" as const,

3
src/types/canvas.ts

@ -7,10 +7,12 @@
export type PanMode = "space" | "middleMouse" | "always";
export type ZoomMode = "scroll" | "altScroll" | "ctrlScroll";
export type SelectionMode = "rightSpace" | "click" | "altDrag" | "shiftDrag";
export type CanvasInputMode = "mouse" | "trackpad";
export type CanvasTheme = "dark" | "light";
export interface CanvasNavigationSettings {
theme: CanvasTheme;
inputMode: CanvasInputMode;
panMode: PanMode;
zoomMode: ZoomMode;
selectionMode: SelectionMode;
@ -19,6 +21,7 @@ export interface CanvasNavigationSettings {
export const defaultCanvasNavigationSettings: CanvasNavigationSettings = {
theme: "light",
inputMode: "mouse",
panMode: "always",
zoomMode: "scroll",
selectionMode: "rightSpace",

Loading…
Cancel
Save