feat: JsonEditor for inputting JSON content (#4)

* feat(editor): add JsonEditor component for JSON configuration editing

* fix(provider): update API Key visibility logic in ProviderForm component

* fix(editor): stabilize JsonEditor height and restore API Key logic

- Revert API Key visibility to preset-based rule and injection to non-custom preset only.
- Reduce JsonEditor min-height from rows*22px to rows*18px to lessen layout jitter when switching presets.
- Keep fonts/size consistent with the previous textarea for visual parity.

* - fix(form): remove websiteUrl auto-extraction from JSON to prevent incorrect overrides

---------

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
QuentinHsu
2025-09-06 09:30:09 +08:00
committed by GitHub
parent 07dd70570e
commit a14f7ef9b2
5 changed files with 272 additions and 31 deletions

View File

@@ -0,0 +1,97 @@
import React, { useRef, useEffect } from "react";
import { EditorView, basicSetup } from "codemirror";
import { json } from "@codemirror/lang-json";
import { oneDark } from "@codemirror/theme-one-dark";
import { EditorState } from "@codemirror/state";
import { placeholder } from "@codemirror/view";
interface JsonEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
darkMode?: boolean;
rows?: number;
}
const JsonEditor: React.FC<JsonEditorProps> = ({
value,
onChange,
placeholder: placeholderText = "",
darkMode = false,
rows = 12,
}) => {
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
useEffect(() => {
if (!editorRef.current) return;
// 创建编辑器扩展
const minHeightPx = Math.max(1, rows) * 18; // 降低最小高度以减少抖动
const sizingTheme = EditorView.theme({
"&": { minHeight: `${minHeightPx}px` },
".cm-scroller": { overflow: "auto" },
".cm-content": {
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
fontSize: "14px",
},
});
const extensions = [
basicSetup,
json(),
placeholder(placeholderText || ""),
sizingTheme,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newValue = update.state.doc.toString();
onChange(newValue);
}
}),
];
// 如果启用深色模式,添加深色主题
if (darkMode) {
extensions.push(oneDark);
}
// 创建初始状态
const state = EditorState.create({
doc: value,
extensions,
});
// 创建编辑器视图
const view = new EditorView({
state,
parent: editorRef.current,
});
viewRef.current = view;
// 清理函数
return () => {
view.destroy();
viewRef.current = null;
};
}, [darkMode, rows]); // 依赖项中不包含 onChange 和 placeholder避免不必要的重建
// 当 value 从外部改变时更新编辑器内容
useEffect(() => {
if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
const transaction = viewRef.current.state.update({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: value,
},
});
viewRef.current.dispatch(transaction);
}
}, [value]);
return <div ref={editorRef} style={{ width: "100%" }} />;
};
export default JsonEditor;

View File

@@ -4,7 +4,6 @@ import { AppType } from "../lib/tauri-api";
import {
updateCoAuthoredSetting,
checkCoAuthoredSetting,
extractWebsiteUrl,
getApiKeyFromConfig,
hasApiKeyField,
setApiKeyInConfig,
@@ -12,6 +11,7 @@ import {
import { providerPresets } from "../config/providerPresets";
import { codexProviderPresets } from "../config/codexProviderPresets";
import "./AddProviderModal.css";
import JsonEditor from "./JsonEditor";
interface ProviderFormProps {
appType?: AppType;
@@ -160,9 +160,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const { name, value } = e.target;
if (name === "settingsConfig") {
// 当用户修改配置时,尝试自动提取官网地址
const extractedWebsiteUrl = extractWebsiteUrl(value);
// 同时检查并同步选择框状态
const hasCoAuthoredDisabled = checkCoAuthoredSetting(value);
setDisableCoAuthored(hasCoAuthoredDisabled);
@@ -171,12 +168,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const parsedKey = getApiKeyFromConfig(value);
setApiKey(parsedKey);
setFormData({
...formData,
// 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容
setFormData((prev) => ({
...prev,
[name]: value,
// 只有在官网地址为空时才自动填入
websiteUrl: formData.websiteUrl || extractedWebsiteUrl,
});
}));
} else {
setFormData({
...formData,
@@ -609,7 +605,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onChange={(e) => setCodexConfig(e.target.value)}
placeholder={``}
rows={8}
style={{ fontFamily: "monospace", fontSize: "14px" }}
/>
<small className="field-hint">
Codex config.toml
@@ -632,11 +627,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
Claude Code
</label>
</div>
<textarea
id="settingsConfig"
name="settingsConfig"
<JsonEditor
value={formData.settingsConfig}
onChange={handleChange}
onChange={(value) => handleChange({
target: { name: "settingsConfig", value }
} as React.ChangeEvent<HTMLTextAreaElement>)}
placeholder={`{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
@@ -644,8 +639,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
}
}`}
rows={12}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/>
<small className="field-hint">
Claude Code settings.json

View File

@@ -33,21 +33,6 @@ export const checkCoAuthoredSetting = (jsonString: string): boolean => {
}
};
// 从JSON配置中提取并处理官网地址
export const extractWebsiteUrl = (jsonString: string): string => {
try {
const config = JSON.parse(jsonString);
const baseUrl = config?.env?.ANTHROPIC_BASE_URL;
if (baseUrl && typeof baseUrl === "string") {
// 去掉 "api." 前缀
return baseUrl.replace(/^https?:\/\/api\./, "https://");
}
} catch (err) {
// 忽略JSON解析错误
}
return "";
};
// 读取配置中的 API Keyenv.ANTHROPIC_AUTH_TOKEN
export const getApiKeyFromConfig = (jsonString: string): string => {