Commit c9ab97c3 authored by edy's avatar edy

fix(ui): scope settings actions

parent 68462773
......@@ -71,6 +71,15 @@ import {
} from "./features/shell/startupStatus";
import { SettingsPanels } from "./features/settings/SettingsPanels";
import { SettingsView } from "./features/settings/SettingsView";
import {
getHasPendingSettingsChange,
getResetCopywritingSettingsDrafts,
getResetDigitalHumanSettingsDrafts,
getResetDouyinRuntimeSettingsDrafts,
getResetImageSettingsDrafts,
getResetVideoSettingsDrafts,
getResetXhsFeishuSettingsDrafts
} from "./features/settings/settingsDrafts";
import { useSaveSettings } from "./features/settings/useSaveSettings";
import { useSettingsState } from "./features/settings/useSettingsState";
import { useSmokeActionHandlers } from "./features/smoke/useSmokeActionHandlers";
......@@ -397,8 +406,7 @@ export default function App() {
saving,
setSaving,
hasPendingLobsterKey,
hasPendingXhsFeishuConfig,
hasPendingModelKeys
hasPendingXhsFeishuConfig
} = useSettingsState(config);
const {
messageTraces,
......@@ -411,8 +419,13 @@ export default function App() {
} = useMessageTraces();
const {
saveConfig,
saveWorkspaceDirectory,
restoreWorkspaceDirectory,
saveLobsterKey,
saveXhsFeishuConfig,
saveCopywritingConfig,
saveImageConfig,
saveVideoConfig,
saveDigitalHumanConfig,
saveDouyinRuntimeConfig,
pickWorkspaceDirectory
} = useSaveSettings({
config,
......@@ -491,6 +504,76 @@ export default function App() {
const savedWorkspacePath = config?.workspacePath ?? "";
const displayedWorkspacePath = workspacePathDraft.trim() || savedWorkspacePath || ui.none;
const hasPendingWorkspacePathChange = Boolean(config && workspacePathDraft.trim() && workspacePathDraft.trim() !== savedWorkspacePath);
const hasPendingBasicConfig = hasPendingLobsterKey || hasPendingWorkspacePathChange;
const hasPendingCopywritingConfig = Boolean(copywritingModelApiKeyDraft.trim());
const hasPendingImageConfig = Boolean(imageModelApiKeyDraft.trim());
const hasPendingVideoConfig = Boolean(videoModelApiKeyDraft.trim());
const hasPendingDigitalHumanConfig = Boolean(
digitalHumanVolcAccessKeyDraft.trim()
|| digitalHumanVolcSecretKeyDraft.trim()
|| digitalHumanQiniuAccessKeyDraft.trim()
|| digitalHumanQiniuSecretKeyDraft.trim()
);
const hasPendingDouyinRuntimeConfig = Boolean(
videoAnalyzerBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.videoAnalyzer.baseUrl ?? "").trim()
|| videoAnalyzerModelIdDraft.trim() !== (config?.douyinRuntimeConfig.videoAnalyzer.modelId ?? "").trim()
|| videoAnalyzerApiKeyDraft.trim()
|| replicationBriefBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.replicationBrief.baseUrl ?? "").trim()
|| replicationBriefModelIdDraft.trim() !== (config?.douyinRuntimeConfig.replicationBrief.modelId ?? "").trim()
|| replicationBriefApiKeyDraft.trim()
|| vectcutBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.vectcut.baseUrl ?? "").trim()
|| vectcutFileBaseUrlDraft.trim() !== (config?.douyinRuntimeConfig.vectcut.fileBaseUrl ?? "").trim()
|| vectcutApiKeyDraft.trim()
);
const hasPendingSettingsChange = getHasPendingSettingsChange({
hasPendingBasicConfig,
hasPendingXhsFeishuConfig,
hasPendingCopywritingConfig,
hasPendingImageConfig,
hasPendingVideoConfig,
hasPendingDigitalHumanConfig,
hasPendingDouyinRuntimeConfig
});
void hasPendingSettingsChange;
const resetXhsFeishuSettingsDrafts = useCallback(() => {
const drafts = getResetXhsFeishuSettingsDrafts();
setXhsFeishuAppIdDraft(drafts.xhsFeishuAppId);
setXhsFeishuAppSecretDraft(drafts.xhsFeishuAppSecret);
setXhsFeishuAppTokenDraft(drafts.xhsFeishuAppToken);
setXhsFeishuTableIdDraft(drafts.xhsFeishuTableId);
}, []);
const resetCopywritingSettingsDrafts = useCallback(() => {
setCopywritingModelApiKeyDraft(getResetCopywritingSettingsDrafts().copywritingModelApiKey);
}, []);
const resetImageSettingsDrafts = useCallback(() => {
setImageModelApiKeyDraft(getResetImageSettingsDrafts().imageModelApiKey);
}, []);
const resetVideoSettingsDrafts = useCallback(() => {
setVideoModelApiKeyDraft(getResetVideoSettingsDrafts().videoModelApiKey);
}, []);
const resetDigitalHumanSettingsDrafts = useCallback(() => {
const drafts = getResetDigitalHumanSettingsDrafts();
setDigitalHumanVolcAccessKeyDraft(drafts.digitalHumanVolcAccessKey);
setDigitalHumanVolcSecretKeyDraft(drafts.digitalHumanVolcSecretKey);
setDigitalHumanQiniuAccessKeyDraft(drafts.digitalHumanQiniuAccessKey);
setDigitalHumanQiniuSecretKeyDraft(drafts.digitalHumanQiniuSecretKey);
}, []);
const resetDouyinRuntimeSettingsDrafts = useCallback(() => {
if (!config) {
return;
}
const drafts = getResetDouyinRuntimeSettingsDrafts(config);
setVideoAnalyzerBaseUrlDraft(drafts.videoAnalyzerBaseUrl);
setVideoAnalyzerModelIdDraft(drafts.videoAnalyzerModelId);
setVideoAnalyzerApiKeyDraft(drafts.videoAnalyzerApiKey);
setReplicationBriefBaseUrlDraft(drafts.replicationBriefBaseUrl);
setReplicationBriefModelIdDraft(drafts.replicationBriefModelId);
setReplicationBriefApiKeyDraft(drafts.replicationBriefApiKey);
setVectcutBaseUrlDraft(drafts.vectcutBaseUrl);
setVectcutFileBaseUrlDraft(drafts.vectcutFileBaseUrl);
setVectcutApiKeyDraft(drafts.vectcutApiKey);
}, [config]);
const startupProgress = getStartupProgress(startupPhase);
const startupCurtainStatus = getStartupCurtainStatus(startupPhase, chatLaunchState);
const startupCurtainFootnote = getStartupCurtainFootnote(chatLaunchState);
......@@ -1230,7 +1313,7 @@ export default function App() {
saving,
bindingLabel: ui.binding,
onLobsterKeyChange: setLobsterKeyDraft,
onSave: () => void saveConfig({ lobsterKey: lobsterKeyDraft })
onSave: () => void saveLobsterKey()
},
hasExpertProjects: Boolean(expertPageProjects.length),
noExpertsLabel: expertsPageCopy.noExperts,
......@@ -1306,12 +1389,16 @@ export default function App() {
config,
workspaceApiKeyConfigured: Boolean(workspace?.apiKeyConfigured),
displayedWorkspacePath,
hasPendingWorkspacePathChange,
saving,
hasPendingLobsterKey,
hasPendingWorkspacePathChange,
hasPendingXhsFeishuConfig,
hasPendingModelKeys,
saving,
labels: { saving: ui.saving, save: ui.save, export: ui.export },
hasPendingCopywritingConfig,
hasPendingImageConfig,
hasPendingVideoConfig,
hasPendingDigitalHumanConfig,
hasPendingDouyinRuntimeConfig,
labels: { export: ui.export },
drafts: {
lobsterKey: lobsterKeyDraft,
xhsFeishuAppId: xhsFeishuAppIdDraft,
......@@ -1358,9 +1445,19 @@ export default function App() {
setVectcutFileBaseUrl: setVectcutFileBaseUrlDraft,
setVectcutApiKey: setVectcutApiKeyDraft
},
saveConfig,
saveWorkspaceDirectory,
restoreWorkspaceDirectory,
onSaveLobsterKey: () => void saveLobsterKey(),
onSaveXhsFeishuConfig: () => void saveXhsFeishuConfig(),
onResetXhsFeishuConfig: resetXhsFeishuSettingsDrafts,
onSaveCopywritingConfig: () => void saveCopywritingConfig(),
onResetCopywritingConfig: resetCopywritingSettingsDrafts,
onSaveImageConfig: () => void saveImageConfig(),
onResetImageConfig: resetImageSettingsDrafts,
onSaveVideoConfig: () => void saveVideoConfig(),
onResetVideoConfig: resetVideoSettingsDrafts,
onSaveDigitalHumanConfig: () => void saveDigitalHumanConfig(),
onResetDigitalHumanConfig: resetDigitalHumanSettingsDrafts,
onSaveDouyinRuntimeConfig: () => void saveDouyinRuntimeConfig(),
onResetDouyinRuntimeConfig: resetDouyinRuntimeSettingsDrafts,
pickWorkspaceDirectory,
exportDiagnostics
} satisfies ComponentProps<typeof SettingsPanels>;
......
......@@ -5,7 +5,10 @@ interface SettingsViewProps {
children: ReactNode
}
export function SettingsView({ statusHint, children }: SettingsViewProps) {
export function SettingsView({
statusHint,
children
}: SettingsViewProps) {
return (
<div className="page-stack settings-page-stack settings-page-shell">
{statusHint}
......
import type { AppConfig } from "@qjclaw/shared-types"
export interface PendingSettingsFlags {
hasPendingBasicConfig: boolean
hasPendingXhsFeishuConfig: boolean
hasPendingCopywritingConfig: boolean
hasPendingImageConfig: boolean
hasPendingVideoConfig: boolean
hasPendingDigitalHumanConfig: boolean
hasPendingDouyinRuntimeConfig: boolean
}
export interface BasicResetSettingsDrafts {
lobsterKey: string
workspacePath: string
}
export interface DigitalHumanResetSettingsDrafts {
digitalHumanVolcAccessKey: string
digitalHumanVolcSecretKey: string
digitalHumanQiniuAccessKey: string
digitalHumanQiniuSecretKey: string
}
export interface DouyinRuntimeResetSettingsDrafts {
videoAnalyzerBaseUrl: string
videoAnalyzerModelId: string
videoAnalyzerApiKey: string
replicationBriefBaseUrl: string
replicationBriefModelId: string
replicationBriefApiKey: string
vectcutBaseUrl: string
vectcutFileBaseUrl: string
vectcutApiKey: string
}
export interface XhsFeishuResetSettingsDrafts {
xhsFeishuAppId: string
xhsFeishuAppSecret: string
xhsFeishuAppToken: string
xhsFeishuTableId: string
}
export function getHasPendingSettingsChange(flags: PendingSettingsFlags) {
return flags.hasPendingBasicConfig
|| flags.hasPendingXhsFeishuConfig
|| flags.hasPendingCopywritingConfig
|| flags.hasPendingImageConfig
|| flags.hasPendingVideoConfig
|| flags.hasPendingDigitalHumanConfig
|| flags.hasPendingDouyinRuntimeConfig
}
export function getResetBasicSettingsDrafts(config: AppConfig): BasicResetSettingsDrafts {
return {
lobsterKey: "",
workspacePath: config.workspacePath
}
}
export function getResetCopywritingSettingsDrafts() {
return {
copywritingModelApiKey: ""
}
}
export function getResetImageSettingsDrafts() {
return {
imageModelApiKey: ""
}
}
export function getResetVideoSettingsDrafts() {
return {
videoModelApiKey: ""
}
}
export function getResetDigitalHumanSettingsDrafts(): DigitalHumanResetSettingsDrafts {
return {
digitalHumanVolcAccessKey: "",
digitalHumanVolcSecretKey: "",
digitalHumanQiniuAccessKey: "",
digitalHumanQiniuSecretKey: ""
}
}
export function getResetDouyinRuntimeSettingsDrafts(config: AppConfig): DouyinRuntimeResetSettingsDrafts {
return {
videoAnalyzerBaseUrl: config.douyinRuntimeConfig.videoAnalyzer.baseUrl,
videoAnalyzerModelId: config.douyinRuntimeConfig.videoAnalyzer.modelId ?? "",
videoAnalyzerApiKey: "",
replicationBriefBaseUrl: config.douyinRuntimeConfig.replicationBrief.baseUrl,
replicationBriefModelId: config.douyinRuntimeConfig.replicationBrief.modelId ?? "",
replicationBriefApiKey: "",
vectcutBaseUrl: config.douyinRuntimeConfig.vectcut.baseUrl,
vectcutFileBaseUrl: config.douyinRuntimeConfig.vectcut.fileBaseUrl,
vectcutApiKey: ""
}
}
export function getResetXhsFeishuSettingsDrafts(): XhsFeishuResetSettingsDrafts {
return {
xhsFeishuAppId: "",
xhsFeishuAppSecret: "",
xhsFeishuAppToken: "",
xhsFeishuTableId: ""
}
}
This diff is collapsed.
import test from "node:test"
import assert from "node:assert/strict"
import type { AppConfig } from "@qjclaw/shared-types"
import {
getHasPendingSettingsChange,
getResetBasicSettingsDrafts,
getResetCopywritingSettingsDrafts,
getResetDigitalHumanSettingsDrafts,
getResetDouyinRuntimeSettingsDrafts,
getResetImageSettingsDrafts,
getResetVideoSettingsDrafts,
getResetXhsFeishuSettingsDrafts
} from "../src/features/settings/settingsDrafts.ts"
const config = {
setupMode: "employee-key",
provider: "openai",
baseUrl: "https://example.test/v1",
apiKeyConfigured: true,
gatewayTokenConfigured: false,
authTokenConfigured: false,
defaultModel: "qwen-max",
workspacePath: "/workspace/current",
gatewayUrl: "ws://127.0.0.1:8765",
cloudApiBaseUrl: "https://cloud.example.test",
runtimeCloudApiBaseUrl: "https://runtime.example.test",
runtimeMode: "bundled-runtime",
expertModelConfig: {
image: { baseUrl: "https://image.example.test", modelId: "image-model", apiKeyConfigured: true },
video: { baseUrl: "https://video.example.test", modelId: "video-model", apiKeyConfigured: true },
copywriting: { baseUrl: "https://copy.example.test", modelId: "copy-model", apiKeyConfigured: true },
digitalHuman: {
volcRegion: "cn-north-1",
volcService: "cv",
volcHost: "visual.volcengineapi.com",
volcScheme: "https",
ttsVoice: "zh-CN-YunxiNeural",
qiniuBucket: "bucket",
qiniuDomain: "https://cdn.example.test",
qiniuKeyPrefix: "omnihuman",
volcAccessKeyConfigured: true,
volcSecretKeyConfigured: true,
qiniuAccessKeyConfigured: true,
qiniuSecretKeyConfigured: true
}
},
douyinRuntimeConfig: {
videoAnalyzer: { baseUrl: "https://video-analyzer.example.test", modelId: "va-model", apiKeyConfigured: true },
replicationBrief: { baseUrl: "https://brief.example.test", modelId: "brief-model", apiKeyConfigured: true },
vectcut: { baseUrl: "https://vectcut.example.test", fileBaseUrl: "https://files.example.test", apiKeyConfigured: true }
},
xhsFeishuConfig: {
appIdConfigured: true,
appSecretConfigured: true,
appTokenConfigured: true,
tableIdConfigured: true
}
} satisfies AppConfig
test("detects any pending settings change", () => {
assert.equal(getHasPendingSettingsChange({
hasPendingBasicConfig: false,
hasPendingXhsFeishuConfig: false,
hasPendingCopywritingConfig: false,
hasPendingImageConfig: false,
hasPendingVideoConfig: false,
hasPendingDigitalHumanConfig: false,
hasPendingDouyinRuntimeConfig: false
}), false)
assert.equal(getHasPendingSettingsChange({
hasPendingBasicConfig: false,
hasPendingXhsFeishuConfig: false,
hasPendingCopywritingConfig: false,
hasPendingImageConfig: false,
hasPendingVideoConfig: false,
hasPendingDigitalHumanConfig: false,
hasPendingDouyinRuntimeConfig: true
}), true)
})
test("basic reset only affects lobster key and workspace path", () => {
assert.deepEqual(getResetBasicSettingsDrafts(config), {
lobsterKey: "",
workspacePath: "/workspace/current"
})
})
test("single model resets only clear the current module key", () => {
assert.deepEqual(getResetCopywritingSettingsDrafts(), {
copywritingModelApiKey: "",
})
assert.deepEqual(getResetImageSettingsDrafts(), {
imageModelApiKey: "",
})
assert.deepEqual(getResetVideoSettingsDrafts(), {
videoModelApiKey: "",
})
})
test("xhs feishu reset only clears its own drafts", () => {
assert.deepEqual(getResetXhsFeishuSettingsDrafts(), {
xhsFeishuAppId: "",
xhsFeishuAppSecret: "",
xhsFeishuAppToken: "",
xhsFeishuTableId: ""
})
})
test("digital human reset only clears its own secrets", () => {
assert.deepEqual(getResetDigitalHumanSettingsDrafts(), {
digitalHumanVolcAccessKey: "",
digitalHumanVolcSecretKey: "",
digitalHumanQiniuAccessKey: "",
digitalHumanQiniuSecretKey: ""
})
})
test("douyin runtime reset restores saved base urls and model ids while clearing runtime secrets", () => {
assert.deepEqual(getResetDouyinRuntimeSettingsDrafts(config), {
videoAnalyzerBaseUrl: "https://video-analyzer.example.test",
videoAnalyzerModelId: "va-model",
videoAnalyzerApiKey: "",
replicationBriefBaseUrl: "https://brief.example.test",
replicationBriefModelId: "brief-model",
replicationBriefApiKey: "",
vectcutBaseUrl: "https://vectcut.example.test",
vectcutFileBaseUrl: "https://files.example.test",
vectcutApiKey: ""
})
})
import test from "node:test"
import assert from "node:assert/strict"
import { readFileSync } from "node:fs"
const settingsPanelsSource = readFileSync(new URL("../src/features/settings/SettingsPanels.tsx", import.meta.url), "utf8")
const settingsStylesSource = readFileSync(new URL("../src/styles/settings.css", import.meta.url), "utf8")
test("settings panels remove per-module status chips", () => {
assert.doesNotMatch(settingsPanelsSource, /StatusChip/)
assert.doesNotMatch(settingsPanelsSource, /已配置/)
assert.doesNotMatch(settingsPanelsSource, /未配置/)
})
test("settings page buttons use the unified action button classes", () => {
assert.match(settingsPanelsSource, /settings-action-button settings-action-button-secondary/)
assert.match(settingsPanelsSource, /settings-action-button settings-action-button-primary/)
assert.doesNotMatch(settingsPanelsSource, /settings-card-action-button/)
assert.doesNotMatch(settingsPanelsSource, /settings-secondary-button/)
assert.doesNotMatch(settingsStylesSource, /\.settings-panel \.status-chip/)
assert.doesNotMatch(settingsStylesSource, /\.settings-card-action-button/)
assert.match(settingsStylesSource, /\.settings-action-button\b/)
assert.match(settingsStylesSource, /\.settings-actions-row\b/)
})
test("basic config action buttons share the same width rule", () => {
assert.match(
settingsStylesSource,
/\.settings-inline-save-button,\s*\n\.settings-basic-directory-actions \.settings-action-button\s*\{\s*\n\s*width:\s*96px;\s*\n\s*min-width:\s*96px;/m
)
})
test("workspace directory keeps only export diagnostics and change directory actions stacked", () => {
assert.doesNotMatch(settingsPanelsSource, />\s*恢复当前\s*</)
assert.doesNotMatch(settingsPanelsSource, />\s*保存目录\s*</)
assert.match(settingsPanelsSource, /onClick=\{\(\) => void exportDiagnostics\(\)\}/)
assert.match(settingsPanelsSource, /onClick=\{\(\) => void pickWorkspaceDirectory\(\)\}/)
assert.match(
settingsStylesSource,
/\.settings-basic-directory-actions\s*\{[\s\S]*?flex-direction:\s*column;[\s\S]*?align-items:\s*stretch;/m
)
})
test("xhs feishu actions are anchored inside the input block", () => {
assert.match(settingsPanelsSource, /className="settings-xhs-feishu-form"/)
assert.match(settingsPanelsSource, /renderActions\(\{\s*hasPending:\s*hasPendingXhsFeishuConfig,\s*onReset:\s*\(\)\s*=>\s*void onResetXhsFeishuConfig\(\),\s*onSave:\s*\(\)\s*=>\s*void onSaveXhsFeishuConfig\(\),\s*className:\s*"settings-xhs-feishu-actions"/m)
assert.match(
settingsStylesSource,
/\.settings-xhs-feishu-form\s*\{[\s\S]*?display:\s*grid;[\s\S]*?gap:\s*8px;/m
)
assert.match(
settingsStylesSource,
/\.settings-xhs-feishu-actions\s*\{[\s\S]*?justify-content:\s*flex-end;[\s\S]*?margin-top:\s*0;/m
)
})
test("settings panel actions are wired per section instead of global handlers", () => {
assert.doesNotMatch(settingsPanelsSource, /\bhasPendingSettingsChange\b/)
assert.doesNotMatch(settingsPanelsSource, /\bonSaveAll\b/)
assert.match(settingsPanelsSource, /\bonSaveLobsterKey\b/)
assert.doesNotMatch(settingsPanelsSource, /\bsaveWorkspaceDirectory\b/)
assert.doesNotMatch(settingsPanelsSource, /\brestoreWorkspaceDirectory\b/)
assert.match(settingsPanelsSource, /\bonSaveCopywritingConfig\b/)
assert.match(settingsPanelsSource, /\bonSaveImageConfig\b/)
assert.match(settingsPanelsSource, /\bonSaveVideoConfig\b/)
assert.match(settingsPanelsSource, /\bonSaveDigitalHumanConfig\b/)
assert.match(settingsPanelsSource, /\bonSaveDouyinRuntimeConfig\b/)
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment