@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.
- package/README.MD +21 -0
- package/build/index.cjs.js +21 -21
- package/build/index.es.js +164 -15
- package/build/style.css +1 -1
- package/package.json +1 -1
- package/src/ai/LocalAIDrawer.jsx +184 -39
- package/src/ai/bridgeClient.js +6 -0
package/src/ai/LocalAIDrawer.jsx
CHANGED
|
@@ -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"> </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
|
|
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
|
-
{
|
|
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-
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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-
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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-
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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}
|
package/src/ai/bridgeClient.js
CHANGED
|
@@ -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',
|