@cniot/mdd-editor 0.3.3 → 0.3.5

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.
@@ -8,6 +8,7 @@ import {
8
8
  openPageWorkspace,
9
9
  pullPage,
10
10
  pushPage,
11
+ rollbackPage,
11
12
  shouldUseBridgeRelay,
12
13
  } from './bridgeClient';
13
14
  import { buildPageIR, getPageCode } from './pageIR';
@@ -16,6 +17,7 @@ import { EVENT_KEY } from '$src/common/const';
16
17
  const MIN_BRIDGE_VERSION = '0.1.4';
17
18
  const START_COMMAND = `npm i -g @cniot/mdd-ai-bridge
18
19
  mdd-ai-bridge`;
20
+ const trimEndSlash = (value = '') => value.replace(/\/+$/, '');
19
21
 
20
22
  const parseSchema = (value) => {
21
23
  if (!value) return null;
@@ -100,11 +102,84 @@ const getChangeTypeText = (type) => {
100
102
  }
101
103
  };
102
104
 
105
+ const TOKEN_PATTERN =
106
+ /(\/\/.*|\/\*.*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b(?:async|await|break|case|catch|class|const|default|else|export|false|finally|for|from|function|if|import|let|new|null|return|switch|throw|true|try|undefined|var|while)\b|\b\d+(?:\.\d+)?\b|[#.][a-zA-Z_-][\w-]*|@[a-zA-Z-]+|[{}()[\],;:])/g;
107
+
108
+ const getTokenClassName = (token, language) => {
109
+ if (/^\/\//.test(token) || /^\/\*/.test(token)) return 'tok-comment';
110
+ if (/^["'`]/.test(token)) return language === 'json' && token.endsWith('"') ? 'tok-string' : 'tok-string';
111
+ if (/^\d/.test(token)) return 'tok-number';
112
+ if (/^(#|\.|@)/.test(token)) return 'tok-selector';
113
+ if (/^[{}()[\],;:]$/.test(token)) return 'tok-punc';
114
+ return 'tok-keyword';
115
+ };
116
+
117
+ const renderHighlightedCode = (line = '', language = 'text') => {
118
+ if (!line) return <span className="tok-empty">&nbsp;</span>;
119
+ const nodes = [];
120
+ let lastIndex = 0;
121
+ let match = TOKEN_PATTERN.exec(line);
122
+ while (match) {
123
+ if (match.index > lastIndex) {
124
+ nodes.push(line.slice(lastIndex, match.index));
125
+ }
126
+ const token = match[0];
127
+ nodes.push(
128
+ <span className={getTokenClassName(token, language)} key={`${match.index}-${token}`}>
129
+ {token}
130
+ </span>,
131
+ );
132
+ lastIndex = match.index + token.length;
133
+ match = TOKEN_PATTERN.exec(line);
134
+ }
135
+ if (lastIndex < line.length) {
136
+ nodes.push(line.slice(lastIndex));
137
+ }
138
+ TOKEN_PATTERN.lastIndex = 0;
139
+ return nodes;
140
+ };
141
+
142
+ function SideBySideDiff({ files = [] }) {
143
+ if (!files.length) return null;
144
+ return (
145
+ <div className="mdd-local-ai-side-diff">
146
+ {files.map((file) => (
147
+ <div className="mdd-local-ai-side-file" key={file.fileName}>
148
+ <div className="mdd-local-ai-side-file-head">
149
+ <span>{file.fileName}</span>
150
+ <span>
151
+ -{file.stats?.removed || 0} / +{file.stats?.added || 0}
152
+ </span>
153
+ </div>
154
+ <div className="mdd-local-ai-side-grid">
155
+ <div className="mdd-local-ai-side-col-head">上次发送</div>
156
+ <div className="mdd-local-ai-side-col-head">本地修改</div>
157
+ {(file.rows || []).map((row, index) => (
158
+ <React.Fragment key={`${file.fileName}-${index}`}>
159
+ <div className={`mdd-local-ai-side-cell is-before is-${row.type}`}>
160
+ <span className="mdd-local-ai-side-line">{row.before?.lineNumber || ''}</span>
161
+ <code>{renderHighlightedCode(row.before?.content || '', file.language)}</code>
162
+ </div>
163
+ <div className={`mdd-local-ai-side-cell is-after is-${row.type}`}>
164
+ <span className="mdd-local-ai-side-line">{row.after?.lineNumber || ''}</span>
165
+ <code>{renderHighlightedCode(row.after?.content || '', file.language)}</code>
166
+ </div>
167
+ </React.Fragment>
168
+ ))}
169
+ </div>
170
+ </div>
171
+ ))}
172
+ </div>
173
+ );
174
+ }
175
+
103
176
  function PullDiffDetail({ diffSummary }) {
104
177
  if (!diffSummary?.hasBaseline) return null;
105
178
  const schemaChanges = diffSummary.schema?.changes || diffSummary.schema?.paths || [];
106
179
  const diffText = diffSummary.diffText || '';
107
- const hasDetail = schemaChanges.length > 0 || diffSummary.script?.changed || diffSummary.style?.changed || diffText;
180
+ const diffFiles = diffSummary.diffFiles || [];
181
+ const hasDetail =
182
+ schemaChanges.length > 0 || diffSummary.script?.changed || diffSummary.style?.changed || diffText || diffFiles.length;
108
183
  if (!hasDetail) return null;
109
184
 
110
185
  return (
@@ -149,7 +224,8 @@ function PullDiffDetail({ diffSummary }) {
149
224
  </div>
150
225
  ) : null}
151
226
 
152
- {diffText ? <pre className="mdd-local-ai-diff-pre">{diffText}</pre> : null}
227
+ {diffFiles.length ? <SideBySideDiff files={diffFiles} /> : null}
228
+ {!diffFiles.length && diffText ? <pre className="mdd-local-ai-diff-pre">{diffText}</pre> : null}
153
229
  </div>
154
230
  </details>
155
231
  );
@@ -301,6 +377,38 @@ export default function LocalAIDrawer(props) {
301
377
  }
302
378
  };
303
379
 
380
+ const handleRollback = async () => {
381
+ const confirmed = window.confirm(
382
+ '确认回滚到上次“发送到本地 AI”时的线上基线吗?回滚会先更新当前编辑器内容,确认预览后还需要使用原保存按钮提交到线上。',
383
+ );
384
+ if (!confirmed) return;
385
+
386
+ setLoadingAction('rollback');
387
+ setPullError(null);
388
+ try {
389
+ const res = await rollbackPage(baseURL, pageCode);
390
+ const nextSchema = parseSchema(res?.schemaInfo);
391
+ const nextScriptInfo = {
392
+ script: res?.scriptInfo || '',
393
+ style: res?.style || '',
394
+ };
395
+ applySchemaToEditor(schema, nextSchema);
396
+ schema.emit(EVENT_KEY.SCRIPT_UPDATE, nextScriptInfo);
397
+ onApply?.({
398
+ schemaJson: nextSchema,
399
+ scriptInfo: nextScriptInfo,
400
+ raw: res,
401
+ });
402
+ setLastSummary(res?.summary || '已回滚到上次发送到本地 AI 时的基线');
403
+ setLastDiffSummary(null);
404
+ Message.success('已回滚上次 AI 修改,请预览后保存');
405
+ } catch (e) {
406
+ Message.error(e.message || '回滚上次更改失败');
407
+ } finally {
408
+ setLoadingAction('');
409
+ }
410
+ };
411
+
304
412
  const handleStatus = async () => {
305
413
  setLoadingAction('status');
306
414
  try {
@@ -329,6 +437,10 @@ export default function LocalAIDrawer(props) {
329
437
  }
330
438
  };
331
439
 
440
+ const handleOpenBridgeHome = React.useCallback(() => {
441
+ window.open(`${trimEndSlash(baseURL)}/`, '_blank', 'noopener,noreferrer');
442
+ }, [baseURL]);
443
+
332
444
  React.useEffect(() => {
333
445
  if (shouldUseBridgeRelay(baseURL)) {
334
446
  setStatus('点击按钮后通过本地 relay 连接');
@@ -339,53 +451,86 @@ export default function LocalAIDrawer(props) {
339
451
 
340
452
  return (
341
453
  <div className="mdd-local-ai">
342
- <CnCard className="mdd-local-ai-card">
343
- <div className="mdd-local-ai-row">
344
- <span className="mdd-local-ai-label">Bridge</span>
345
- <Input
346
- size="small"
347
- value={baseURL}
348
- onChange={setBaseURL}
349
- placeholder={DEFAULT_BRIDGE_URL}
350
- style={{ width: 320 }}
351
- />
454
+ <CnCard className="mdd-local-ai-card mdd-local-ai-hero">
455
+ <div className="mdd-local-ai-hero-head">
456
+ <div>
457
+ <div className="mdd-local-ai-heading">本地 AI 工作区</div>
458
+ <div className="mdd-local-ai-subtitle">把当前页面同步成本地文件,再交给 Cursor、Qoder、Codex CLI 修改。</div>
459
+ </div>
460
+ <span className={bridgeInfo?.version ? 'mdd-local-ai-status is-ready' : 'mdd-local-ai-status'}>
461
+ {bridgeInfo?.version ? `Bridge v${bridgeInfo.version}` : status}
462
+ </span>
463
+ </div>
464
+
465
+ <div className="mdd-local-ai-bridge-row">
466
+ <div className="mdd-local-ai-field">
467
+ <span className="mdd-local-ai-label">Bridge 地址</span>
468
+ <Input
469
+ size="small"
470
+ value={baseURL}
471
+ onChange={setBaseURL}
472
+ placeholder={DEFAULT_BRIDGE_URL}
473
+ className="mdd-local-ai-input"
474
+ />
475
+ </div>
352
476
  <CnButton size="small" loading={loadingAction === 'health'} onClick={checkHealth}>
353
477
  检测连接
354
478
  </CnButton>
479
+ <CnButton size="small" onClick={handleOpenBridgeHome}>
480
+ 访问本地 AI 主页
481
+ </CnButton>
355
482
  </div>
483
+
356
484
  <div className="mdd-local-ai-meta">
357
- <div>状态:{status}</div>
358
- {bridgeInfo?.version ? <div>Bridge 版本:{bridgeInfo.version}</div> : null}
359
- <div>页面:{pageCode}</div>
360
- {workspacePath ? <div>目录:{workspacePath}</div> : null}
485
+ <div className="mdd-local-ai-meta-item">
486
+ <span>状态</span>
487
+ <strong>{status}</strong>
488
+ </div>
489
+ <div className="mdd-local-ai-meta-item">
490
+ <span>页面</span>
491
+ <strong>{pageCode}</strong>
492
+ </div>
493
+ <div className="mdd-local-ai-meta-item mdd-local-ai-meta-path">
494
+ <span>目录</span>
495
+ <strong>{workspacePath || expectedWorkspacePath}</strong>
496
+ </div>
361
497
  </div>
362
498
  </CnCard>
363
499
 
364
- <div className="mdd-local-ai-actions">
365
- <CnButton type="primary" loading={loadingAction === 'push'} onClick={handlePush}>
366
- 发送到本地 AI
367
- </CnButton>
368
- <CnButton loading={loadingAction === 'pull'} onClick={handlePull}>
369
- 同步本地修改
370
- </CnButton>
371
- <CnButton loading={loadingAction === 'status'} onClick={handleStatus}>
372
- 查看状态
373
- </CnButton>
374
- <CnButton loading={loadingAction === 'open'} onClick={handleOpen}>
375
- 打开目录
376
- </CnButton>
500
+ <div className="mdd-local-ai-section">
501
+ <div className="mdd-local-ai-section-title">工作区操作</div>
502
+ <div className="mdd-local-ai-actions">
503
+ <CnButton type="primary" loading={loadingAction === 'push'} onClick={handlePush}>
504
+ 发送到本地 AI
505
+ </CnButton>
506
+ <CnButton loading={loadingAction === 'pull'} onClick={handlePull}>
507
+ 同步本地修改
508
+ </CnButton>
509
+ <CnButton loading={loadingAction === 'status'} onClick={handleStatus}>
510
+ 查看状态
511
+ </CnButton>
512
+ <CnButton loading={loadingAction === 'open'} onClick={handleOpen}>
513
+ 打开目录
514
+ </CnButton>
515
+ <CnButton loading={loadingAction === 'rollback'} onClick={handleRollback}>
516
+ 回滚上次更改
517
+ </CnButton>
518
+ </div>
377
519
  </div>
378
520
 
379
- <div className="mdd-local-ai-copy-actions">
380
- <CnButton size="small" onClick={() => handleCopy(START_COMMAND, '已复制启动命令')}>
381
- 复制启动命令
382
- </CnButton>
383
- <CnButton size="small" onClick={() => handleCopy(expectedWorkspacePath, '已复制工作区路径')}>
384
- 复制工作区路径
385
- </CnButton>
386
- <CnButton size="small" onClick={() => handleCopy(aiPrompt, '已复制 AI 提示词')}>
387
- 复制 AI 提示词
388
- </CnButton>
521
+ <div className="mdd-local-ai-section">
522
+ <div className="mdd-local-ai-section-title">辅助操作</div>
523
+ <div className="mdd-local-ai-copy-actions">
524
+ <CnButton size="small" onClick={() => handleCopy(START_COMMAND, '已复制启动命令')}>
525
+ 复制启动命令
526
+ </CnButton>
527
+ <CnButton size="small" onClick={() => handleCopy(expectedWorkspacePath, '已复制工作区路径')}>
528
+ 复制工作区路径
529
+ </CnButton>
530
+ <CnButton size="small" onClick={() => handleCopy(aiPrompt, '已复制 AI 提示词')}>
531
+ 复制 AI 提示词
532
+ </CnButton>
533
+ </div>
389
534
  </div>
390
535
 
391
536
  {lastSummary ? <div className="mdd-local-ai-summary">{lastSummary}</div> : null}
@@ -207,6 +207,12 @@ export function pullPage(baseURL, code) {
207
207
  });
208
208
  }
209
209
 
210
+ export function rollbackPage(baseURL, code) {
211
+ return request(baseURL, `/pages/${encodeURIComponent(code)}/rollback`, {
212
+ method: 'POST',
213
+ });
214
+ }
215
+
210
216
  export function getPageStatus(baseURL, code) {
211
217
  return request(baseURL, `/pages/${encodeURIComponent(code)}/status`, {
212
218
  method: 'GET',