@agentaily/design-system 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import React, { useRef, useState, useEffect } from "react";
2
+ import React, { useState, useRef, useEffect } from "react";
3
3
  import { Alert } from "../feedback/Alert.js";
4
4
  import { Badge } from "../display/Badge.js";
5
5
  import { BrandMark } from "../utilities/BrandMark.js";
@@ -9,6 +9,7 @@ import { Icon } from "../utilities/Icon.js";
9
9
  import { IconButton } from "../buttons/IconButton.js";
10
10
  import { SecretField } from "../inputs/SecretField.js";
11
11
  import { Select } from "../inputs/Select.js";
12
+ import { Spinner } from "../feedback/Spinner.js";
12
13
  import { StatusPill } from "../display/StatusPill.js";
13
14
  import { Switch } from "../inputs/Switch.js";
14
15
  import { TestRow } from "./TestRow.js";
@@ -129,7 +130,49 @@ const S_FIELD_MAP = [
129
130
  { from: null, to: "提交时间", tag: "自动" },
130
131
  { from: null, to: "来源链接", tag: "自动" }
131
132
  ];
132
- function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_KEY }) {
133
+ const S_DEFAULTS = {
134
+ dsKey: "",
135
+ dsModel: "deepseek-chat",
136
+ capOn: false,
137
+ cap: "200",
138
+ dsStatus: "idle",
139
+ dsResult: "",
140
+ appId: "",
141
+ secret: "",
142
+ link: "",
143
+ fsStatus: "idle",
144
+ fsResult: "",
145
+ saved: false
146
+ };
147
+ function s_normalize(raw) {
148
+ const r = raw || {};
149
+ return {
150
+ dsKey: r.dsKey || "",
151
+ dsModel: r.dsModel || "deepseek-chat",
152
+ capOn: !!r.capOn,
153
+ cap: r.cap || "200",
154
+ dsStatus: r.dsStatus === "ok" ? "ok" : "idle",
155
+ dsResult: r.dsResult || "",
156
+ appId: r.appId || "",
157
+ secret: r.secret || "",
158
+ link: r.link || "",
159
+ fsStatus: r.fsStatus === "ok" ? "ok" : "idle",
160
+ fsResult: r.fsResult || "",
161
+ saved: !!r.saved
162
+ };
163
+ }
164
+ function IntegrationSettings({
165
+ onClose,
166
+ showUsageCap = true,
167
+ storageKey = S_LS_KEY,
168
+ value,
169
+ onChange,
170
+ onSave,
171
+ onTest,
172
+ readiness,
173
+ masked
174
+ }) {
175
+ const controlled = value !== void 0;
133
176
  const load = () => {
134
177
  try {
135
178
  return JSON.parse(localStorage.getItem(storageKey)) || {};
@@ -137,20 +180,33 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
137
180
  return {};
138
181
  }
139
182
  };
140
- const boot = useRef(load()).current;
141
- const [dsKey, setDsKey] = useState(boot.dsKey || "");
142
- const [dsModel, setDsModel] = useState(boot.dsModel || "deepseek-chat");
143
- const [capOn, setCapOn] = useState(!!boot.capOn);
144
- const [cap, setCap] = useState(boot.cap || "200");
145
- const [dsStatus, setDsStatus] = useState(boot.dsStatus === "ok" ? "ok" : "idle");
146
- const [dsResult, setDsResult] = useState(boot.dsResult || "");
147
- const [appId, setAppId] = useState(boot.appId || "");
148
- const [secret, setSecret] = useState(boot.secret || "");
149
- const [link, setLink] = useState(boot.link || "");
150
- const [fsStatus, setFsStatus] = useState(boot.fsStatus === "ok" ? "ok" : "idle");
151
- const [fsResult, setFsResult] = useState(boot.fsResult || "");
183
+ const [localCfg, setLocalCfg] = useState(() => controlled ? S_DEFAULTS : s_normalize(load()));
184
+ const cfg = controlled ? { ...S_DEFAULTS, ...value } : localCfg;
185
+ const {
186
+ dsKey,
187
+ dsModel,
188
+ capOn,
189
+ cap,
190
+ dsStatus,
191
+ dsResult,
192
+ appId,
193
+ secret,
194
+ link,
195
+ fsStatus,
196
+ fsResult,
197
+ saved
198
+ } = cfg;
199
+ const cfgRef = useRef(cfg);
200
+ cfgRef.current = cfg;
201
+ const update = (partial) => {
202
+ if (controlled) {
203
+ if (onChange) onChange({ ...cfgRef.current, ...partial });
204
+ } else setLocalCfg((c) => ({ ...c, ...partial }));
205
+ };
152
206
  const [dirty, setDirty] = useState(false);
153
- const [saved, setSaved] = useState(!!boot.saved);
207
+ const [saving, setSaving] = useState(false);
208
+ const [dsKeyEdited, setDsKeyEdited] = useState(false);
209
+ const [secretEdited, setSecretEdited] = useState(false);
154
210
  useEffect(() => {
155
211
  const onKey = (e) => {
156
212
  if (e.key === "Escape") onClose && onClose();
@@ -158,124 +214,130 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
158
214
  window.addEventListener("keydown", onKey);
159
215
  return () => window.removeEventListener("keydown", onKey);
160
216
  }, [onClose]);
161
- const parsed = s_parseFeishu(link);
162
- const dsConnected = dsStatus === "ok";
163
- const fsConnected = fsStatus === "ok";
164
- const readyCount = (dsConnected ? 1 : 0) + (fsConnected ? 1 : 0);
165
- const allReady = dsConnected && fsConnected;
166
217
  useEffect(() => {
167
- const snap = {
168
- dsKey,
169
- dsModel,
170
- capOn,
171
- cap,
172
- dsStatus,
173
- dsResult,
174
- appId,
175
- secret,
176
- link,
177
- fsStatus,
178
- fsResult,
179
- saved
180
- };
218
+ if (controlled) return;
181
219
  try {
182
- localStorage.setItem(storageKey, JSON.stringify(snap));
220
+ localStorage.setItem(storageKey, JSON.stringify(localCfg));
183
221
  } catch (e) {
184
222
  }
185
- }, [
186
- dsKey,
187
- dsModel,
188
- capOn,
189
- cap,
190
- dsStatus,
191
- dsResult,
192
- appId,
193
- secret,
194
- link,
195
- fsStatus,
196
- fsResult,
197
- saved,
198
- storageKey
199
- ]);
200
- const touch = () => {
223
+ }, [controlled, localCfg, storageKey]);
224
+ const parsed = s_parseFeishu(link);
225
+ const dsConnected = readiness ? !!readiness.deepseek : dsStatus === "ok";
226
+ const fsConnected = readiness ? !!readiness.feishu : fsStatus === "ok";
227
+ const dsPill = dsStatus === "testing" || dsStatus === "error" ? dsStatus : dsConnected ? "ok" : "idle";
228
+ const fsPill = fsStatus === "testing" || fsStatus === "error" ? fsStatus : fsConnected ? "ok" : "idle";
229
+ const readyCount = (dsConnected ? 1 : 0) + (fsConnected ? 1 : 0);
230
+ const allReady = dsConnected && fsConnected;
231
+ const maskedDs = !!(masked && masked.deepseek) && !dsKeyEdited;
232
+ const maskedFs = !!(masked && masked.feishu) && !secretEdited;
233
+ const setCfg = (partial) => {
234
+ update({ ...partial, saved: false });
201
235
  setDirty(true);
202
- setSaved(false);
203
236
  };
204
- const editDs = (setter) => (v) => {
205
- setter(v);
206
- if (dsStatus !== "idle") {
207
- setDsStatus("idle");
208
- setDsResult("");
237
+ const editDs = (key) => (v) => {
238
+ const patch = { [key]: v, saved: false };
239
+ if (cfgRef.current.dsStatus !== "idle") {
240
+ patch.dsStatus = "idle";
241
+ patch.dsResult = "";
209
242
  }
210
- touch();
243
+ update(patch);
244
+ setDirty(true);
211
245
  };
212
- const editFs = (setter) => (v) => {
213
- setter(v);
214
- if (fsStatus !== "idle") {
215
- setFsStatus("idle");
216
- setFsResult("");
246
+ const editFs = (key) => (v) => {
247
+ const patch = { [key]: v, saved: false };
248
+ if (cfgRef.current.fsStatus !== "idle") {
249
+ patch.fsStatus = "idle";
250
+ patch.fsResult = "";
217
251
  }
218
- touch();
252
+ update(patch);
253
+ setDirty(true);
219
254
  };
220
- const testDeepSeek = async () => {
221
- setDsStatus("testing");
222
- setDsResult("");
223
- await s_sleep(1300);
224
- const k = dsKey.trim();
225
- if (k.startsWith("sk-") && k.length >= 20) {
226
- setDsStatus("ok");
227
- setDsResult(`连接正常 · 延迟 0.4s · ${dsModel}`);
255
+ const editDsKey = (v) => {
256
+ if (!dsKeyEdited) setDsKeyEdited(true);
257
+ editDs("dsKey")(v);
258
+ };
259
+ const editSecret = (v) => {
260
+ if (!secretEdited) setSecretEdited(true);
261
+ editFs("secret")(v);
262
+ };
263
+ const setTest = (which, status, result, finalize) => {
264
+ const patch = which === "deepseek" ? { dsStatus: status, dsResult: result } : { fsStatus: status, fsResult: result };
265
+ if (finalize) patch.saved = false;
266
+ update(patch);
267
+ if (finalize) setDirty(true);
268
+ };
269
+ const builtinTest = async (which) => {
270
+ if (which === "deepseek") {
271
+ setTest("deepseek", "testing", "", false);
272
+ await s_sleep(1300);
273
+ const k = cfgRef.current.dsKey.trim();
274
+ if (k.startsWith("sk-") && k.length >= 20)
275
+ setTest("deepseek", "ok", `连接正常 · 延迟 0.4s · ${cfgRef.current.dsModel}`, true);
276
+ else
277
+ setTest(
278
+ "deepseek",
279
+ "error",
280
+ k ? "密钥无效或额度不足,请核对后重试" : "请先填写 API Key",
281
+ true
282
+ );
228
283
  } else {
229
- setDsStatus("error");
230
- setDsResult(k ? "密钥无效或额度不足,请核对后重试" : "请先填写 API Key");
284
+ setTest("feishu", "testing", "", false);
285
+ await s_sleep(1500);
286
+ const c = cfgRef.current;
287
+ const p = s_parseFeishu(c.link);
288
+ if (!c.appId.trim() || !c.secret.trim())
289
+ return setTest("feishu", "error", "缺少 App ID 或 App Secret", true);
290
+ if (!p) return setTest("feishu", "error", "无法识别多维表格链接,请粘贴完整 URL", true);
291
+ if (!p.table) return setTest("feishu", "error", "链接缺少数据表 (table) 参数", true);
292
+ setTest("feishu", "ok", "已连接 ·「报名登记表」· 检测到 6 个字段", true);
231
293
  }
232
- touch();
233
294
  };
234
- const testFeishu = async () => {
235
- setFsStatus("testing");
236
- setFsResult("");
237
- await s_sleep(1500);
238
- if (!appId.trim() || !secret.trim()) {
239
- setFsStatus("error");
240
- setFsResult("缺少 App ID 或 App Secret");
241
- touch();
295
+ const runTest = async (which) => {
296
+ if (onTest) {
297
+ setTest(which, "testing", "", false);
298
+ try {
299
+ const res = await onTest(which);
300
+ const ok = !!(res && res.ok);
301
+ setTest(
302
+ which,
303
+ ok ? "ok" : "error",
304
+ res && res.message || (ok ? "连接正常" : "连接失败"),
305
+ true
306
+ );
307
+ } catch (e) {
308
+ setTest(which, "error", e && e.message || "测试失败", true);
309
+ }
242
310
  return;
243
311
  }
244
- if (!parsed) {
245
- setFsStatus("error");
246
- setFsResult("无法识别多维表格链接,请粘贴完整 URL");
247
- touch();
248
- return;
249
- }
250
- if (!parsed.table) {
251
- setFsStatus("error");
252
- setFsResult("链接缺少数据表 (table) 参数");
253
- touch();
254
- return;
255
- }
256
- setFsStatus("ok");
257
- setFsResult("已连接 ·「报名登记表」· 检测到 6 个字段");
258
- touch();
312
+ builtinTest(which);
259
313
  };
260
- const onSave = () => {
261
- setSaved(true);
314
+ const afterSaved = () => {
262
315
  setDirty(false);
316
+ setDsKeyEdited(false);
317
+ setSecretEdited(false);
318
+ };
319
+ const doSave = async () => {
320
+ if (onSave) {
321
+ setSaving(true);
322
+ try {
323
+ await onSave(cfgRef.current);
324
+ update({ saved: true });
325
+ afterSaved();
326
+ } catch (e) {
327
+ } finally {
328
+ setSaving(false);
329
+ }
330
+ } else {
331
+ update({ saved: true });
332
+ afterSaved();
333
+ }
263
334
  };
264
335
  const onDiscard = () => {
265
- const b = load();
266
- setDsKey(b.dsKey || "");
267
- setDsModel(b.dsModel || "deepseek-chat");
268
- setCapOn(!!b.capOn);
269
- setCap(b.cap || "200");
270
- setDsStatus(b.dsStatus === "ok" ? "ok" : "idle");
271
- setDsResult(b.dsResult || "");
272
- setAppId(b.appId || "");
273
- setSecret(b.secret || "");
274
- setLink(b.link || "");
275
- setFsStatus(b.fsStatus === "ok" ? "ok" : "idle");
276
- setFsResult(b.fsResult || "");
277
- setSaved(!!b.saved);
336
+ setDsKeyEdited(false);
337
+ setSecretEdited(false);
278
338
  setDirty(false);
339
+ if (controlled) return;
340
+ setLocalCfg(s_normalize(load()));
279
341
  };
280
342
  return /* @__PURE__ */ jsx("div", { className: "s-overlay", role: "dialog", "aria-modal": "true", "aria-label": "集成设置", children: /* @__PURE__ */ jsxs("div", { className: "s-modal", children: [
281
343
  /* @__PURE__ */ jsxs("header", { className: "s-modal__bar", children: [
@@ -318,13 +380,13 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
318
380
  /* @__PURE__ */ jsxs(
319
381
  "section",
320
382
  {
321
- className: "s-card" + (dsStatus === "ok" ? " is-ok" : dsStatus === "error" ? " is-error" : ""),
383
+ className: "s-card" + (dsPill === "ok" ? " is-ok" : dsPill === "error" ? " is-error" : ""),
322
384
  children: [
323
385
  /* @__PURE__ */ jsxs("div", { className: "s-card__head", children: [
324
386
  /* @__PURE__ */ jsxs("div", { className: "s-card__toprow", children: [
325
387
  /* @__PURE__ */ jsx("div", { className: "s-card__icon", children: /* @__PURE__ */ jsx(Icon, { name: "key", size: 16 }) }),
326
388
  /* @__PURE__ */ jsx("span", { className: "ax-label s-card__eyebrow", children: "对话引擎 · LLM" }),
327
- /* @__PURE__ */ jsx("span", { className: "s-card__status", children: /* @__PURE__ */ jsx(StatusPill, { status: dsStatus }) })
389
+ /* @__PURE__ */ jsx("span", { className: "s-card__status", children: /* @__PURE__ */ jsx(StatusPill, { status: dsPill }) })
328
390
  ] }),
329
391
  /* @__PURE__ */ jsx("h2", { className: "s-card__title", children: "DeepSeek" }),
330
392
  /* @__PURE__ */ jsx("p", { className: "s-card__desc", children: "驱动对话式交互。用户发送的每一条消息,都通过这把密钥调用 DeepSeek 补全。" })
@@ -335,9 +397,10 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
335
397
  {
336
398
  label: "API KEY",
337
399
  value: dsKey,
338
- onChange: editDs(setDsKey),
339
- placeholder: "sk-xxxxxxxxxxxxxxxxxxxxxxxx",
340
- error: dsStatus === "error" && !dsKey.trim() ? "此项必填" : void 0
400
+ onChange: editDsKey,
401
+ placeholder: maskedDs ? "已保存 ········ · 留空则保持不变" : "sk-xxxxxxxxxxxxxxxxxxxxxxxx",
402
+ hint: maskedDs ? "已存密钥 · 留空表示不修改,输入新值即覆盖" : void 0,
403
+ error: !maskedDs && dsStatus === "error" && !dsKey.trim() ? "此项必填" : void 0
341
404
  }
342
405
  ),
343
406
  /* @__PURE__ */ jsxs("div", { className: "s-lock", children: [
@@ -354,7 +417,7 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
354
417
  Select,
355
418
  {
356
419
  value: dsModel,
357
- onChange: (e) => editDs(setDsModel)(e.target.value),
420
+ onChange: (e) => editDs("dsModel")(e.target.value),
358
421
  options: [
359
422
  { value: "deepseek-chat", label: "deepseek-chat · 通用 · 快" },
360
423
  { value: "deepseek-reasoner", label: "deepseek-reasoner · 深度推理" }
@@ -378,10 +441,7 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
378
441
  {
379
442
  label: "启用每月用量上限",
380
443
  checked: capOn,
381
- onChange: (e) => {
382
- setCapOn(e.target.checked);
383
- touch();
384
- }
444
+ onChange: (e) => setCfg({ capOn: e.target.checked })
385
445
  }
386
446
  ),
387
447
  /* @__PURE__ */ jsxs("div", { className: "s-cap__field", style: { display: capOn ? "flex" : "none" }, children: [
@@ -392,10 +452,7 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
392
452
  type: "text",
393
453
  inputMode: "numeric",
394
454
  value: cap,
395
- onChange: (e) => {
396
- setCap(e.target.value.replace(/[^0-9]/g, ""));
397
- touch();
398
- },
455
+ onChange: (e) => setCfg({ cap: e.target.value.replace(/[^0-9]/g, "") }),
399
456
  "aria-label": "每月上限(元)"
400
457
  }
401
458
  ),
@@ -433,8 +490,8 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
433
490
  {
434
491
  status: dsStatus,
435
492
  result: dsResult,
436
- onTest: testDeepSeek,
437
- disabled: !dsKey.trim(),
493
+ onTest: () => runTest("deepseek"),
494
+ disabled: !dsKey.trim() && !maskedDs,
438
495
  idleHint: "填写密钥后测试连通性"
439
496
  }
440
497
  )
@@ -444,13 +501,13 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
444
501
  /* @__PURE__ */ jsxs(
445
502
  "section",
446
503
  {
447
- className: "s-card" + (fsStatus === "ok" ? " is-ok" : fsStatus === "error" ? " is-error" : ""),
504
+ className: "s-card" + (fsPill === "ok" ? " is-ok" : fsPill === "error" ? " is-error" : ""),
448
505
  children: [
449
506
  /* @__PURE__ */ jsxs("div", { className: "s-card__head", children: [
450
507
  /* @__PURE__ */ jsxs("div", { className: "s-card__toprow", children: [
451
508
  /* @__PURE__ */ jsx("div", { className: "s-card__icon", children: /* @__PURE__ */ jsx(Icon, { name: "table", size: 16 }) }),
452
509
  /* @__PURE__ */ jsx("span", { className: "ax-label s-card__eyebrow", children: "数据写入 · FEISHU BITABLE" }),
453
- /* @__PURE__ */ jsx("span", { className: "s-card__status", children: /* @__PURE__ */ jsx(StatusPill, { status: fsStatus }) })
510
+ /* @__PURE__ */ jsx("span", { className: "s-card__status", children: /* @__PURE__ */ jsx(StatusPill, { status: fsPill }) })
454
511
  ] }),
455
512
  /* @__PURE__ */ jsx("h2", { className: "s-card__title", children: "飞书多维表格" }),
456
513
  /* @__PURE__ */ jsx("p", { className: "s-card__desc", children: "每次提交后,数据自动写入指定多维表格的一行。需要一个飞书自建应用的凭证,以及目标表格的链接。" })
@@ -467,7 +524,7 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
467
524
  value: appId,
468
525
  spellCheck: "false",
469
526
  placeholder: "cli_xxxxxxxxxxxx",
470
- onChange: (e) => editFs(setAppId)(e.target.value)
527
+ onChange: (e) => editFs("appId")(e.target.value)
471
528
  }
472
529
  ),
473
530
  /* @__PURE__ */ jsx("p", { className: "s-field__hint", children: "应用标识,可公开。" })
@@ -477,9 +534,9 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
477
534
  {
478
535
  label: "App Secret",
479
536
  value: secret,
480
- onChange: editFs(setSecret),
481
- placeholder: "••••••••••••••••",
482
- hint: "应用密钥,加密存储。"
537
+ onChange: editSecret,
538
+ placeholder: maskedFs ? "已保存 ········ · 留空则保持不变" : "••••••••••••••••",
539
+ hint: maskedFs ? "已存密钥 · 留空表示不修改" : "应用密钥,加密存储。"
483
540
  }
484
541
  )
485
542
  ] }),
@@ -493,7 +550,7 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
493
550
  value: link,
494
551
  spellCheck: "false",
495
552
  placeholder: "https://your-team.feishu.cn/base/bascn…?table=tbl…",
496
- onChange: (e) => editFs(setLink)(e.target.value)
553
+ onChange: (e) => editFs("link")(e.target.value)
497
554
  }
498
555
  ),
499
556
  /* @__PURE__ */ jsx("p", { className: "s-field__hint", children: "在多维表格右上角「分享」中复制链接粘贴即可,App Token 与数据表会自动识别。" })
@@ -586,8 +643,8 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
586
643
  {
587
644
  status: fsStatus,
588
645
  result: fsResult,
589
- onTest: testFeishu,
590
- disabled: !appId.trim() || !secret.trim() || !parsed,
646
+ onTest: () => runTest("feishu"),
647
+ disabled: !appId.trim() || !secret.trim() && !maskedFs || !parsed,
591
648
  idleHint: "填写凭证与链接后测试写入权限"
592
649
  }
593
650
  )
@@ -614,9 +671,10 @@ function IntegrationSettings({ onClose, showUsageCap = true, storageKey = S_LS_K
614
671
  {
615
672
  variant: "primary",
616
673
  size: "md",
617
- icon: /* @__PURE__ */ jsx(Icon, { name: "save", size: 14 }),
618
- disabled: !allReady || !dirty,
619
- onClick: onSave,
674
+ icon: saving ? /* @__PURE__ */ jsx(Spinner, { size: "sm" }) : /* @__PURE__ */ jsx(Icon, { name: "save", size: 14 }),
675
+ disabled: !allReady || !dirty || saving,
676
+ "aria-busy": saving || void 0,
677
+ onClick: doSave,
620
678
  children: "保存配置"
621
679
  }
622
680
  )