feat(ui): add merge preview to common config editor components

Add real-time merge preview functionality to all config editor components
(Claude, Codex, Gemini) that shows the final merged configuration when
common config is enabled.

UI changes:
- Add finalConfig/finalEnv prop to display merged result
- Add toggle button to show/hide merge preview (Eye/EyeOff icons)
- Auto-show preview when common config is enabled
- Display custom config editor with label when preview is visible
- Show read-only merged preview below custom config
- Add visual indicators: "只读" badge, green hint text

The preview helps users understand how their custom configuration
combines with the shared common config template before saving.
This commit is contained in:
YoVinchen
2026-01-26 14:09:39 +08:00
parent 2ff329f6c0
commit b3b3c0732a
5 changed files with 279 additions and 31 deletions
@@ -30,6 +30,9 @@ interface CodexConfigEditorProps {
onExtract?: () => void;
isExtracting?: boolean;
/** 最终合并后的配置(只读预览) */
finalConfig?: string;
}
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
@@ -47,6 +50,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
configError,
onExtract,
isExtracting,
finalConfig,
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
@@ -76,6 +80,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
commonConfigError={commonConfigError}
configError={configError}
finalConfig={finalConfig}
/>
{/* Common Config Modal */}
@@ -1,6 +1,8 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Eye, EyeOff } from "lucide-react";
import JsonEditor from "@/components/JsonEditor";
import { Label } from "@/components/ui/label";
interface CodexAuthSectionProps {
value: string;
@@ -83,6 +85,8 @@ interface CodexConfigSectionProps {
onEditCommonConfig: () => void;
commonConfigError?: string;
configError?: string;
/** 最终合并后的配置(只读预览) */
finalConfig?: string;
}
/**
@@ -96,9 +100,11 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
onEditCommonConfig,
commonConfigError,
configError,
finalConfig,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -115,6 +121,13 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
return () => observer.disconnect();
}, []);
// 当启用通用配置时,自动显示预览
useEffect(() => {
if (useCommonConfig && finalConfig) {
setShowPreview(true);
}
}, [useCommonConfig, finalConfig]);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
@@ -136,7 +149,32 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
</label>
</div>
<div className="flex items-center justify-end">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{useCommonConfig && finalConfig && (
<button
type="button"
onClick={() => setShowPreview(!showPreview)}
className="inline-flex items-center gap-1 text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{showPreview ? (
<>
<EyeOff className="w-3 h-3" />
{t("codexConfig.hidePreview", {
defaultValue: "隐藏合并预览",
})}
</>
) : (
<>
<Eye className="w-3 h-3" />
{t("codexConfig.showPreview", {
defaultValue: "显示合并预览",
})}
</>
)}
</button>
)}
</div>
<button
type="button"
onClick={onEditCommonConfig}
@@ -152,15 +190,57 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
</p>
)}
<JsonEditor
value={value}
onChange={onChange}
placeholder=""
darkMode={isDarkMode}
rows={8}
showValidation={false}
language="javascript"
/>
{/* 自定义配置编辑器 */}
<div className="space-y-1">
{useCommonConfig && showPreview && (
<Label className="text-xs text-muted-foreground">
{t("codexConfig.customConfig", {
defaultValue: "自定义配置(覆盖通用配置)",
})}
</Label>
)}
<JsonEditor
value={value}
onChange={onChange}
placeholder=""
darkMode={isDarkMode}
rows={useCommonConfig && showPreview ? 6 : 8}
showValidation={false}
language="javascript"
/>
</div>
{/* 合并预览(只读)- 放在自定义配置下面 */}
{useCommonConfig && showPreview && finalConfig && (
<div className="space-y-1">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">
{t("codexConfig.mergedPreview", {
defaultValue: "合并预览(只读)",
})}
</Label>
<span className="text-xs text-green-500 dark:text-green-400">
{t("codexConfig.mergedPreviewHint", {
defaultValue: "通用配置 + 自定义配置 = 最终配置",
})}
</span>
</div>
<div className="relative">
<JsonEditor
value={finalConfig}
onChange={() => {}} // 只读
darkMode={isDarkMode}
rows={6}
showValidation={false}
language="javascript"
readOnly={true}
/>
<div className="absolute top-2 right-2 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded">
{t("common.readonly", { defaultValue: "只读" })}
</div>
</div>
</div>
)}
{configError && (
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Save, Download, Loader2 } from "lucide-react";
import { Save, Download, Loader2, Eye, EyeOff } from "lucide-react";
import JsonEditor from "@/components/JsonEditor";
interface CommonConfigEditorProps {
@@ -19,6 +19,8 @@ interface CommonConfigEditorProps {
onModalClose: () => void;
onExtract?: () => void;
isExtracting?: boolean;
/** 最终合并后的配置(只读预览) */
finalConfig?: string;
}
export function CommonConfigEditor({
@@ -34,9 +36,11 @@ export function CommonConfigEditor({
onModalClose,
onExtract,
isExtracting,
finalConfig,
}: CommonConfigEditorProps) {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -53,6 +57,13 @@ export function CommonConfigEditor({
return () => observer.disconnect();
}, []);
// 当启用通用配置时,自动显示预览
useEffect(() => {
if (useCommonConfig && finalConfig) {
setShowPreview(true);
}
}, [useCommonConfig, finalConfig]);
return (
<>
<div className="space-y-2">
@@ -75,7 +86,32 @@ export function CommonConfigEditor({
</label>
</div>
</div>
<div className="flex items-center justify-end">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{useCommonConfig && finalConfig && (
<button
type="button"
onClick={() => setShowPreview(!showPreview)}
className="inline-flex items-center gap-1 text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{showPreview ? (
<>
<EyeOff className="w-3 h-3" />
{t("claudeConfig.hidePreview", {
defaultValue: "隐藏合并预览",
})}
</>
) : (
<>
<Eye className="w-3 h-3" />
{t("claudeConfig.showPreview", {
defaultValue: "显示合并预览",
})}
</>
)}
</button>
)}
</div>
<button
type="button"
onClick={onEditClick}
@@ -91,20 +127,63 @@ export function CommonConfigEditor({
{commonConfigError}
</p>
)}
<JsonEditor
value={value}
onChange={onChange}
placeholder={`{
{/* 自定义配置编辑器 */}
<div className="space-y-1">
{useCommonConfig && showPreview && (
<Label className="text-xs text-muted-foreground">
{t("claudeConfig.customConfig", {
defaultValue: "自定义配置(覆盖通用配置)",
})}
</Label>
)}
<JsonEditor
value={value}
onChange={onChange}
placeholder={`{
"env": {
"ANTHROPIC_BASE_URL": "https://your-api-endpoint.com",
"ANTHROPIC_AUTH_TOKEN": "your-api-key-here"
}
}`}
darkMode={isDarkMode}
rows={14}
showValidation={true}
language="json"
/>
darkMode={isDarkMode}
rows={useCommonConfig && showPreview ? 10 : 14}
showValidation={true}
language="json"
/>
</div>
{/* 合并预览(只读)- 放在自定义配置下面 */}
{useCommonConfig && showPreview && finalConfig && (
<div className="space-y-1">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">
{t("claudeConfig.mergedPreview", {
defaultValue: "合并预览(只读)",
})}
</Label>
<span className="text-xs text-green-500 dark:text-green-400">
{t("claudeConfig.mergedPreviewHint", {
defaultValue: "通用配置 + 自定义配置 = 最终配置",
})}
</span>
</div>
<div className="relative">
<JsonEditor
value={finalConfig}
onChange={() => {}} // 只读
darkMode={isDarkMode}
rows={8}
showValidation={false}
language="json"
readOnly={true}
/>
<div className="absolute top-2 right-2 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded">
{t("common.readonly", { defaultValue: "只读" })}
</div>
</div>
</div>
)}
</div>
<FullScreenPanel
@@ -17,6 +17,8 @@ interface GeminiConfigEditorProps {
configError: string;
onExtract?: () => void;
isExtracting?: boolean;
/** 最终合并后的 env 配置(只读预览) */
finalEnv?: string;
}
const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
@@ -34,6 +36,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
configError,
onExtract,
isExtracting,
finalEnv,
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
@@ -56,6 +59,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
onCommonConfigToggle={onCommonConfigToggle}
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
commonConfigError={commonConfigError}
finalEnv={finalEnv}
/>
{/* Config JSON Section */}
@@ -1,6 +1,8 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Eye, EyeOff } from "lucide-react";
import JsonEditor from "@/components/JsonEditor";
import { Label } from "@/components/ui/label";
interface GeminiEnvSectionProps {
value: string;
@@ -11,6 +13,8 @@ interface GeminiEnvSectionProps {
onCommonConfigToggle: (checked: boolean) => void;
onEditCommonConfig: () => void;
commonConfigError?: string;
/** 最终合并后的 env 配置(只读预览) */
finalEnv?: string;
}
/**
@@ -25,9 +29,11 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
onCommonConfigToggle,
onEditCommonConfig,
commonConfigError,
finalEnv,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -44,6 +50,13 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
return () => observer.disconnect();
}, []);
// 当启用通用配置时,自动显示预览
useEffect(() => {
if (useCommonConfig && finalEnv) {
setShowPreview(true);
}
}, [useCommonConfig, finalEnv]);
const handleChange = (newValue: string) => {
onChange(newValue);
if (onBlur) {
@@ -74,7 +87,32 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
</label>
</div>
<div className="flex items-center justify-end">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{useCommonConfig && finalEnv && (
<button
type="button"
onClick={() => setShowPreview(!showPreview)}
className="inline-flex items-center gap-1 text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{showPreview ? (
<>
<EyeOff className="w-3 h-3" />
{t("geminiConfig.hidePreview", {
defaultValue: "隐藏合并预览",
})}
</>
) : (
<>
<Eye className="w-3 h-3" />
{t("geminiConfig.showPreview", {
defaultValue: "显示合并预览",
})}
</>
)}
</button>
)}
</div>
<button
type="button"
onClick={onEditCommonConfig}
@@ -92,17 +130,59 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
</p>
)}
<JsonEditor
value={value}
onChange={handleChange}
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
{/* 自定义配置编辑器 */}
<div className="space-y-1">
{useCommonConfig && showPreview && (
<Label className="text-xs text-muted-foreground">
{t("geminiConfig.customConfig", {
defaultValue: "自定义配置(覆盖通用配置)",
})}
</Label>
)}
<JsonEditor
value={value}
onChange={handleChange}
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
GEMINI_API_KEY=sk-your-api-key-here
GEMINI_MODEL=gemini-3-pro-preview`}
darkMode={isDarkMode}
rows={6}
showValidation={false}
language="javascript"
/>
darkMode={isDarkMode}
rows={useCommonConfig && showPreview ? 4 : 6}
showValidation={false}
language="javascript"
/>
</div>
{/* 合并预览(只读)- 放在自定义配置下面 */}
{useCommonConfig && showPreview && finalEnv && (
<div className="space-y-1">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">
{t("geminiConfig.mergedPreview", {
defaultValue: "合并预览(只读)",
})}
</Label>
<span className="text-xs text-green-500 dark:text-green-400">
{t("geminiConfig.mergedPreviewHint", {
defaultValue: "通用配置 + 自定义配置 = 最终配置",
})}
</span>
</div>
<div className="relative">
<JsonEditor
value={finalEnv}
onChange={() => {}} // 只读
darkMode={isDarkMode}
rows={4}
showValidation={false}
language="javascript"
readOnly={true}
/>
<div className="absolute top-2 right-2 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded">
{t("common.readonly", { defaultValue: "只读" })}
</div>
</div>
</div>
)}
{error && (
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>