@nine-lab/nine-mu 0.1.16 → 0.1.18
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/dist/nine-mu.js +26350 -27
- package/dist/nine-mu.js.map +1 -1
- package/dist/nine-mu.umd.js +1 -1
- package/dist/nine-mu.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/components/NineChat.js +24 -5
- package/src/components/NineDiff.js +532 -0
- package/src/components/NineDiffContainer.js +63 -0
- package/src/components/NineDiffPopup.js +0 -0
- package/src/index.js +7 -5
- package/src/services/NineMuService.js +83 -4
- package/vite.config.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nine-lab/nine-mu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "AI-Driven Full-Stack Code Fabrication Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/nine-mu.umd.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
|
-
"
|
|
36
|
+
"mu",
|
|
37
37
|
"codegen",
|
|
38
38
|
"ai",
|
|
39
39
|
"gemini",
|
|
@@ -40,6 +40,24 @@ export class NineChat extends HTMLElement {
|
|
|
40
40
|
$settings.classList.toggle("expand", clicked.classList.contains('menu-setting'));
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
this.shadowRoot.querySelector(".source-gen")?.addEventListener("click", async e => {
|
|
45
|
+
const command = "";
|
|
46
|
+
const targets = "";
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = await this.#service.generateAll(command, targets, this.#routes);
|
|
50
|
+
|
|
51
|
+
// 4. 성공 처리
|
|
52
|
+
$status.textContent = "✅ 완료";
|
|
53
|
+
nine.alert("소스 생성 성공").rgb();
|
|
54
|
+
this.dispatchEvent(new CustomEvent('nine-mu-completed', { detail: result, bubbles: true }));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// 5. 실패 처리
|
|
57
|
+
$status.textContent = "❌ 실패";
|
|
58
|
+
nine.alert(err.message).rgb().shake();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
// --- [그룹 2: Action] 엔터키 입력 시 서비스 호출 ---
|
|
@@ -68,7 +86,7 @@ export class NineChat extends HTMLElement {
|
|
|
68
86
|
// 4. 성공 처리
|
|
69
87
|
$status.textContent = "✅ 완료";
|
|
70
88
|
nine.alert("소스 생성 성공").rgb();
|
|
71
|
-
this.dispatchEvent(new CustomEvent('nine-
|
|
89
|
+
this.dispatchEvent(new CustomEvent('nine-mu-completed', { detail: result, bubbles: true }));
|
|
72
90
|
} catch (err) {
|
|
73
91
|
// 5. 실패 처리
|
|
74
92
|
$status.textContent = "❌ 실패";
|
|
@@ -84,7 +102,7 @@ export class NineChat extends HTMLElement {
|
|
|
84
102
|
|
|
85
103
|
this.shadowRoot.innerHTML = `
|
|
86
104
|
<style>
|
|
87
|
-
@import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-
|
|
105
|
+
@import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
|
|
88
106
|
${customImport}
|
|
89
107
|
</style>
|
|
90
108
|
<div class="wrapper">
|
|
@@ -92,7 +110,7 @@ export class NineChat extends HTMLElement {
|
|
|
92
110
|
<div class="container">
|
|
93
111
|
<div class="head">
|
|
94
112
|
<div class="logo"><span></span></div>
|
|
95
|
-
<div id="status-tag" style="font-size:10px; color:#ff5722; padding:5px;">
|
|
113
|
+
<div id="status-tag" style="font-size:10px; color:#ff5722; padding:5px;">nine-mu Engine Ready</div>
|
|
96
114
|
</div>
|
|
97
115
|
<nx-ai-chat></nx-ai-chat>
|
|
98
116
|
<div class="foot">
|
|
@@ -109,9 +127,10 @@ export class NineChat extends HTMLElement {
|
|
|
109
127
|
</div>
|
|
110
128
|
<div class="menu">
|
|
111
129
|
<div class="collapse-icon"></div>
|
|
112
|
-
<div class="menu-icon menu-filter
|
|
113
|
-
<div class="menu-icon menu-general"></div>
|
|
130
|
+
<div class="menu-icon menu-filter"></div>
|
|
131
|
+
<div class="menu-icon menu-general active"></div>
|
|
114
132
|
<div class="menu-icon menu-setting"></div>
|
|
133
|
+
<div class="menu-icon source-gen"></div>
|
|
115
134
|
</div>
|
|
116
135
|
</div>
|
|
117
136
|
<div class="expand-icon"></div>
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { trace } from "@nine-lab/nine-util";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EditorView, lineNumbers, highlightSpecialChars, drawSelection,
|
|
5
|
+
dropCursor, keymap, highlightActiveLine, highlightActiveLineGutter,
|
|
6
|
+
Decoration, WidgetType
|
|
7
|
+
} from "@codemirror/view";
|
|
8
|
+
import {
|
|
9
|
+
EditorState, Compartment, StateField,
|
|
10
|
+
RangeSetBuilder,
|
|
11
|
+
StateEffect,
|
|
12
|
+
Text
|
|
13
|
+
} from "@codemirror/state";
|
|
14
|
+
import { history, historyKeymap, indentWithTab, selectAll, defaultKeymap } from "@codemirror/commands";
|
|
15
|
+
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
|
|
16
|
+
import { indentOnInput, bracketMatching, syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
|
|
17
|
+
import { javascript } from "@codemirror/lang-javascript";
|
|
18
|
+
import { lintKeymap } from "@codemirror/lint";
|
|
19
|
+
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
|
20
|
+
|
|
21
|
+
import { diff_match_patch } from 'diff-match-patch';
|
|
22
|
+
|
|
23
|
+
// --- Diff 데코레이션을 위한 StateEffect 정의 ---
|
|
24
|
+
const setAsisDecorationsEffect = StateEffect.define();
|
|
25
|
+
const setTobeDecorationsEffect = StateEffect.define();
|
|
26
|
+
|
|
27
|
+
// --- 버튼 데코레이션을 위한 새로운 StateEffect 정의 ---
|
|
28
|
+
const setAsisButtonDecorationsEffect = StateEffect.define();
|
|
29
|
+
const setTobeButtonDecorationsEffect = StateEffect.define();
|
|
30
|
+
|
|
31
|
+
// --- Diff 데코레이션을 위한 StateField 정의 ---
|
|
32
|
+
const asisDiffDecorations = StateField.define({
|
|
33
|
+
create() {
|
|
34
|
+
return Decoration.none;
|
|
35
|
+
},
|
|
36
|
+
update(decorations, tr) {
|
|
37
|
+
decorations = decorations.map(tr.changes);
|
|
38
|
+
for (let effect of tr.effects) {
|
|
39
|
+
if (effect.is(setAsisDecorationsEffect)) {
|
|
40
|
+
return effect.value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return decorations;
|
|
44
|
+
},
|
|
45
|
+
provide: f => EditorView.decorations.from(f)
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const tobeDiffDecorations = StateField.define({
|
|
49
|
+
create() {
|
|
50
|
+
return Decoration.none;
|
|
51
|
+
},
|
|
52
|
+
update(decorations, tr) {
|
|
53
|
+
decorations = decorations.map(tr.changes);
|
|
54
|
+
for (let effect of tr.effects) {
|
|
55
|
+
if (effect.is(setTobeDecorationsEffect)) {
|
|
56
|
+
return effect.value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return decorations;
|
|
60
|
+
},
|
|
61
|
+
provide: f => EditorView.decorations.from(f)
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// --- 버튼 데코레이션을 위한 새로운 StateField 정의 ---
|
|
65
|
+
const asisButtonDecorations = StateField.define({
|
|
66
|
+
create() {
|
|
67
|
+
return Decoration.none;
|
|
68
|
+
},
|
|
69
|
+
update(decorations, tr) {
|
|
70
|
+
decorations = decorations.map(tr.changes);
|
|
71
|
+
for (let effect of tr.effects) {
|
|
72
|
+
if (effect.is(setAsisButtonDecorationsEffect)) {
|
|
73
|
+
return effect.value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return decorations;
|
|
77
|
+
},
|
|
78
|
+
provide: f => EditorView.decorations.from(f)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const tobeButtonDecorations = StateField.define({
|
|
82
|
+
create() {
|
|
83
|
+
return Decoration.none;
|
|
84
|
+
},
|
|
85
|
+
update(decorations, tr) {
|
|
86
|
+
decorations = decorations.map(tr.changes);
|
|
87
|
+
for (let effect of tr.effects) {
|
|
88
|
+
if (effect.is(setTobeButtonDecorationsEffect)) {
|
|
89
|
+
return effect.value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return decorations;
|
|
93
|
+
},
|
|
94
|
+
provide: f => EditorView.decorations.from(f)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// NineDiff 클래스 외부 (파일 상단 or 하단)에 추가
|
|
99
|
+
class MergeButtonWidget extends WidgetType {
|
|
100
|
+
constructor(isAsisButton, textToApply, targetEditorView, diffRange, hostComponent) {
|
|
101
|
+
super();
|
|
102
|
+
this.isAsisButton = isAsisButton; // 이 버튼이 ASIS 에디터에 붙는 버튼인가 (true) TOBE 에디터에 붙는 버튼인가 (false)
|
|
103
|
+
this.textToApply = textToApply; // 적용할 텍스트
|
|
104
|
+
this.targetEditorView = targetEditorView; // 텍스트를 적용할 에디터 뷰 (상대편 에디터)
|
|
105
|
+
this.diffRange = diffRange; // 대상 에디터에서 변경이 일어날 정확한 from/to 오프셋
|
|
106
|
+
this.hostComponent = hostComponent; // NineDiff 인스턴스 참조
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 위젯이 차지할 공간을 텍스트 줄에 할당할지 여부. false로 설정하여 버튼이 줄 사이에 끼어들지 않도록 함.
|
|
110
|
+
eq(other) { return false; }
|
|
111
|
+
|
|
112
|
+
// 위젯의 DOM 요소를 생성합니다.
|
|
113
|
+
toDOM(view) {
|
|
114
|
+
const button = document.createElement("button");
|
|
115
|
+
// SVN 기준 명칭으로 변경
|
|
116
|
+
if (this.isAsisButton) {
|
|
117
|
+
// ASIS (왼쪽) 에디터에 붙는 버튼: AI가 삭제한 것을 '삭제' (ASIS에서 제거)
|
|
118
|
+
button.className = "cm-merge-button revert"; // 'revert' 클래스 사용 (빨간색)
|
|
119
|
+
//button.textContent = "삭제"; // 텍스트 변경
|
|
120
|
+
} else {
|
|
121
|
+
// TOBE (오른쪽) 에디터에 붙는 버튼: AI가 추가한 것을 '적용' (ASIS에 반영)
|
|
122
|
+
button.className = "cm-merge-button accept"; // 'accept' 클래스 사용 (녹색)
|
|
123
|
+
//button.textContent = "← 적용"; // 화살표 방향 변경
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 클릭 이벤트 핸들러
|
|
127
|
+
button.addEventListener("click", () => {
|
|
128
|
+
trace.log(`버튼 클릭: ${this.isAsisButton ? 'ASIS 쪽 (삭제)' : 'TOBE 쪽 (적용)'}`, this.textToApply);
|
|
129
|
+
trace.log("대상 에디터:", this.targetEditorView === this.hostComponent.asisEditorView ? "ASIS" : "TOBE");
|
|
130
|
+
trace.log("대상 범위:", this.diffRange);
|
|
131
|
+
|
|
132
|
+
this.applyChanges(this.textToApply, this.targetEditorView, this.diffRange);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const container = document.createElement("div");
|
|
136
|
+
container.className = "cm-merge-button-container";
|
|
137
|
+
container.appendChild(button);
|
|
138
|
+
return container;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 실제 변경 적용 로직
|
|
142
|
+
applyChanges(text, editorView, range) {
|
|
143
|
+
if (!editorView || !range) {
|
|
144
|
+
trace.error("Target editor view or range is undefined.", editorView, range);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
editorView.dispatch({
|
|
149
|
+
changes: {
|
|
150
|
+
from: range.from,
|
|
151
|
+
to: range.to,
|
|
152
|
+
insert: text
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
requestAnimationFrame(() => {
|
|
157
|
+
this.hostComponent.recalculateDiff();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
export class NineDiff extends HTMLElement {
|
|
164
|
+
//#host;
|
|
165
|
+
#asisEditorView;
|
|
166
|
+
#tobeEditorView;
|
|
167
|
+
#asisEditorEl;
|
|
168
|
+
#tobeEditorEl;
|
|
169
|
+
|
|
170
|
+
#languageCompartment = new Compartment();
|
|
171
|
+
|
|
172
|
+
#isScrollSyncActive = false;
|
|
173
|
+
_asisScrollHandler = null;
|
|
174
|
+
_tobeScrollHandler = null;
|
|
175
|
+
|
|
176
|
+
// MergeButtonWidget에서 접근할 수 있도록 Getter를 공개합니다.
|
|
177
|
+
get asisEditorView() {
|
|
178
|
+
return this.#asisEditorView;
|
|
179
|
+
}
|
|
180
|
+
get tobeEditorView() {
|
|
181
|
+
return this.#tobeEditorView;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
constructor() {
|
|
186
|
+
super();
|
|
187
|
+
this.attachShadow({ mode: 'open' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
connectedCallback() {
|
|
191
|
+
|
|
192
|
+
const customImport = this.getAttribute("css-path") ? `@import "${this.getAttribute("css-path")}";` : "";
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* nine-ai > nine-ai-diff-popup > nx-tab > nine-ai-diff
|
|
196
|
+
*/
|
|
197
|
+
//this.#host = this.getRootNode().host.getRootNode().host.getRootNode().host;
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
this.shadowRoot.innerHTML = `
|
|
201
|
+
<style>
|
|
202
|
+
@import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
|
|
203
|
+
${customImport}
|
|
204
|
+
</style>
|
|
205
|
+
|
|
206
|
+
<div class="wrapper">
|
|
207
|
+
<div class="panel asis"></div>
|
|
208
|
+
<nx-splitter></nx-splitter>
|
|
209
|
+
<div class="panel tobe"></div>
|
|
210
|
+
</div>
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
requestAnimationFrame(() => {
|
|
214
|
+
this.#initCodeMirror();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
disconnectedCallback() {
|
|
219
|
+
if (this._asisScrollHandler) {
|
|
220
|
+
this.#asisEditorView.scrollDOM.removeEventListener('scroll', this._asisScrollHandler);
|
|
221
|
+
this.#tobeEditorView.scrollDOM.removeEventListener('scroll', this._tobeScrollHandler);
|
|
222
|
+
this._asisScrollHandler = null;
|
|
223
|
+
this._tobeScrollHandler = null;
|
|
224
|
+
}
|
|
225
|
+
if (this.#asisEditorView) {
|
|
226
|
+
this.#asisEditorView.destroy();
|
|
227
|
+
}
|
|
228
|
+
if (this.#tobeEditorView) {
|
|
229
|
+
this.#tobeEditorView.destroy();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getContents = () => {
|
|
234
|
+
return (this.#tobeEditorView) ? this.#tobeEditorView.state.doc.toString() : "";
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
#initCodeMirror = () => {
|
|
238
|
+
this.#asisEditorEl = this.shadowRoot.querySelector('.panel.asis');
|
|
239
|
+
this.#tobeEditorEl = this.shadowRoot.querySelector('.panel.tobe');
|
|
240
|
+
|
|
241
|
+
if (!this.#asisEditorEl || !this.#tobeEditorEl) {
|
|
242
|
+
trace.error('CodeMirror panel containers not found!');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const basicExtensions = [
|
|
247
|
+
lineNumbers(),
|
|
248
|
+
highlightSpecialChars(),
|
|
249
|
+
history(),
|
|
250
|
+
drawSelection(),
|
|
251
|
+
dropCursor(),
|
|
252
|
+
EditorState.allowMultipleSelections.of(true),
|
|
253
|
+
indentOnInput(),
|
|
254
|
+
bracketMatching(),
|
|
255
|
+
highlightActiveLine(),
|
|
256
|
+
highlightActiveLineGutter(),
|
|
257
|
+
highlightSelectionMatches(),
|
|
258
|
+
keymap.of([
|
|
259
|
+
...defaultKeymap,
|
|
260
|
+
...searchKeymap,
|
|
261
|
+
...historyKeymap,
|
|
262
|
+
...lintKeymap,
|
|
263
|
+
...completionKeymap,
|
|
264
|
+
indentWithTab,
|
|
265
|
+
selectAll
|
|
266
|
+
]),
|
|
267
|
+
syntaxHighlighting(defaultHighlightStyle),
|
|
268
|
+
autocompletion(),
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
this.#asisEditorView = new EditorView({
|
|
272
|
+
state: EditorState.create({
|
|
273
|
+
doc: '',
|
|
274
|
+
extensions: [
|
|
275
|
+
basicExtensions,
|
|
276
|
+
this.#languageCompartment.of(javascript()),
|
|
277
|
+
// ASIS는 읽기 전용으로 유지 (원본 소스) - 필요에 따라 편집 가능하도록 변경 가능
|
|
278
|
+
EditorState.readOnly.of(false), // ASIS를 편집 가능하게 변경
|
|
279
|
+
asisDiffDecorations,
|
|
280
|
+
asisButtonDecorations,
|
|
281
|
+
EditorView.updateListener.of((update) => {
|
|
282
|
+
if (update.view.contentDOM.firstChild && !update.view._initialAsisContentLoaded) {
|
|
283
|
+
update.view._initialAsisContentLoaded = true;
|
|
284
|
+
trace.log("CodeMirror ASIS view is ready for initial content.");
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
]
|
|
288
|
+
}),
|
|
289
|
+
parent: this.#asisEditorEl
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
this.#tobeEditorView = new EditorView({
|
|
293
|
+
state: EditorState.create({
|
|
294
|
+
doc: '',
|
|
295
|
+
extensions: [
|
|
296
|
+
basicExtensions,
|
|
297
|
+
this.#languageCompartment.of(javascript()),
|
|
298
|
+
EditorState.readOnly.of(true), // TOBE는 AI 추천 소스이므로 읽기 전용 유지
|
|
299
|
+
tobeDiffDecorations,
|
|
300
|
+
tobeButtonDecorations,
|
|
301
|
+
EditorView.updateListener.of((update) => {
|
|
302
|
+
if (update.view.contentDOM.firstChild && !update.view._initialTobeContentLoaded) {
|
|
303
|
+
update.view._initialTobeContentLoaded = true;
|
|
304
|
+
trace.log("CodeMirror TOBE view is ready for initial content.");
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
]
|
|
308
|
+
}),
|
|
309
|
+
parent: this.#tobeEditorEl
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
this.#setupScrollSync();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
#setupScrollSync = () => {
|
|
316
|
+
if (this._asisScrollHandler) {
|
|
317
|
+
this.#asisEditorView.scrollDOM.removeEventListener('scroll', this._asisScrollHandler);
|
|
318
|
+
this.#tobeEditorView.scrollDOM.removeEventListener('scroll', this._tobeScrollHandler);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let scrollingA = false;
|
|
322
|
+
let scrollingB = false;
|
|
323
|
+
|
|
324
|
+
this._asisScrollHandler = () => {
|
|
325
|
+
if (!this.#isScrollSyncActive) return;
|
|
326
|
+
|
|
327
|
+
if (!scrollingB) {
|
|
328
|
+
scrollingA = true;
|
|
329
|
+
this.#tobeEditorView.scrollDOM.scrollTop = this.#asisEditorView.scrollDOM.scrollTop;
|
|
330
|
+
this.#tobeEditorView.scrollDOM.scrollLeft = this.#asisEditorView.scrollDOM.scrollLeft;
|
|
331
|
+
}
|
|
332
|
+
scrollingB = false;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
this._tobeScrollHandler = () => {
|
|
336
|
+
if (!this.#isScrollSyncActive) return;
|
|
337
|
+
|
|
338
|
+
if (!scrollingA) {
|
|
339
|
+
scrollingB = true;
|
|
340
|
+
this.#asisEditorView.scrollDOM.scrollTop = this.#tobeEditorView.scrollDOM.scrollTop;
|
|
341
|
+
this.#asisEditorView.scrollDOM.scrollLeft = this.#tobeEditorView.scrollDOM.scrollLeft;
|
|
342
|
+
}
|
|
343
|
+
scrollingA = false;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
this.#asisEditorView.scrollDOM.addEventListener('scroll', this._asisScrollHandler);
|
|
347
|
+
this.#tobeEditorView.scrollDOM.addEventListener('scroll', this._tobeScrollHandler);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
#applyDiffDecorations = (asisSrc, tobeSrc) => {
|
|
351
|
+
const dmp = new diff_match_patch();
|
|
352
|
+
|
|
353
|
+
const a = dmp.diff_linesToChars_(asisSrc, tobeSrc);
|
|
354
|
+
const lineText1 = a.chars1;
|
|
355
|
+
const lineText2 = a.chars2;
|
|
356
|
+
const lineArray = a.lineArray;
|
|
357
|
+
|
|
358
|
+
const diffs = dmp.diff_main(lineText1, lineText2, true);
|
|
359
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
360
|
+
dmp.diff_charsToLines_(diffs, lineArray);
|
|
361
|
+
|
|
362
|
+
//console.log("Calculated Diffs:", diffs);
|
|
363
|
+
|
|
364
|
+
const asisLineBuilder = new RangeSetBuilder();
|
|
365
|
+
const tobeLineBuilder = new RangeSetBuilder();
|
|
366
|
+
const asisButtonBuilder = new RangeSetBuilder();
|
|
367
|
+
const tobeButtonBuilder = new RangeSetBuilder();
|
|
368
|
+
|
|
369
|
+
let asisCursor = 0;
|
|
370
|
+
let tobeCursor = 0;
|
|
371
|
+
|
|
372
|
+
const insertedLineDeco = Decoration.line({ class: "cm-inserted-line-bg" });
|
|
373
|
+
const deletedLineDeco = Decoration.line({ class: "cm-deleted-line-bg" });
|
|
374
|
+
|
|
375
|
+
const currentInstance = this;
|
|
376
|
+
|
|
377
|
+
for (const [op, text] of diffs) {
|
|
378
|
+
const len = text.length;
|
|
379
|
+
|
|
380
|
+
const asisRangeStart = asisCursor;
|
|
381
|
+
const tobeRangeStart = tobeCursor;
|
|
382
|
+
|
|
383
|
+
switch (op) {
|
|
384
|
+
case diff_match_patch.DIFF_INSERT: // TOBE (AI 추천)에 추가된 내용 (ASIS에는 없음)
|
|
385
|
+
trace.log("DIFF_INSERT (TOBE added):", JSON.stringify(text));
|
|
386
|
+
const tobeLines = text.split('\n');
|
|
387
|
+
for (let i = 0; i < tobeLines.length; i++) {
|
|
388
|
+
if (!(i === tobeLines.length - 1 && tobeLines[i] === '' && text.endsWith('\n'))) {
|
|
389
|
+
tobeLineBuilder.add(tobeCursor, tobeCursor, insertedLineDeco);
|
|
390
|
+
}
|
|
391
|
+
tobeCursor += tobeLines[i].length + (i < tobeLines.length - 1 ? 1 : 0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ⭐️ TOBE (AI 추천) 에디터에 버튼 추가: TOBE의 추가 내용을 ASIS (현재 소스)에 '적용'
|
|
395
|
+
tobeButtonBuilder.add(
|
|
396
|
+
tobeRangeStart,
|
|
397
|
+
tobeRangeStart,
|
|
398
|
+
Decoration.widget({
|
|
399
|
+
widget: new MergeButtonWidget(
|
|
400
|
+
false, // 이 버튼은 TOBE 에디터에 붙는 버튼
|
|
401
|
+
text, // AI가 추가한 내용을 ASIS에 '삽입'
|
|
402
|
+
currentInstance.#asisEditorView, // 대상 에디터는 ASIS (현재 소스)
|
|
403
|
+
{ from: asisRangeStart, to: asisRangeStart + 0 }, // ASIS에서의 '삽입될' 위치
|
|
404
|
+
currentInstance
|
|
405
|
+
),
|
|
406
|
+
side: 1
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case diff_match_patch.DIFF_DELETE: // ASIS (현재 소스)에서 삭제된 내용 (TOBE에는 없음)
|
|
412
|
+
trace.log("DIFF_DELETE (ASIS deleted):", JSON.stringify(text));
|
|
413
|
+
const asisLines = text.split('\n');
|
|
414
|
+
for (let i = 0; i < asisLines.length; i++) {
|
|
415
|
+
if (!(i === asisLines.length - 1 && asisLines[i] === '' && text.endsWith('\n'))) {
|
|
416
|
+
asisLineBuilder.add(asisCursor, asisCursor, deletedLineDeco);
|
|
417
|
+
}
|
|
418
|
+
asisCursor += asisLines[i].length + (i < asisLines.length - 1 ? 1 : 0);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ⭐️ ASIS (현재 소스) 에디터에 버튼 추가: AI의 삭제를 '삭제' (ASIS에서 해당 내용을 제거)
|
|
422
|
+
asisButtonBuilder.add(
|
|
423
|
+
asisRangeStart,
|
|
424
|
+
asisRangeStart,
|
|
425
|
+
Decoration.widget({
|
|
426
|
+
widget: new MergeButtonWidget(
|
|
427
|
+
true, // 이 버튼은 ASIS 에디터에 붙는 버튼
|
|
428
|
+
"", // ASIS에서 해당 내용을 '제거'하므로 삽입할 텍스트는 없음
|
|
429
|
+
currentInstance.#asisEditorView, // 대상 에디터는 ASIS (현재 소스)
|
|
430
|
+
{ from: asisRangeStart, to: asisRangeStart + text.length }, // ASIS에서 '삭제될' 범위
|
|
431
|
+
currentInstance
|
|
432
|
+
),
|
|
433
|
+
side: 1
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
break;
|
|
437
|
+
|
|
438
|
+
case diff_match_patch.DIFF_EQUAL: // 동일한 내용
|
|
439
|
+
asisCursor += len;
|
|
440
|
+
tobeCursor += len;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
asisDecorations: asisLineBuilder.finish(),
|
|
447
|
+
tobeDecorations: tobeLineBuilder.finish(),
|
|
448
|
+
asisButtonDecorations: asisButtonBuilder.finish(),
|
|
449
|
+
tobeButtonDecorations: tobeButtonBuilder.finish()
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
recalculateDiff = () => {
|
|
454
|
+
const asisDoc = this.#asisEditorView.state.doc.toString();
|
|
455
|
+
const tobeDoc = this.#tobeEditorView.state.doc.toString();
|
|
456
|
+
|
|
457
|
+
const { asisDecorations, tobeDecorations, asisButtonDecorations, tobeButtonDecorations } = this.#applyDiffDecorations(asisDoc, tobeDoc);
|
|
458
|
+
|
|
459
|
+
this.#asisEditorView.dispatch({
|
|
460
|
+
effects: [
|
|
461
|
+
setAsisDecorationsEffect.of(asisDecorations),
|
|
462
|
+
setAsisButtonDecorationsEffect.of(asisButtonDecorations)
|
|
463
|
+
]
|
|
464
|
+
});
|
|
465
|
+
this.#tobeEditorView.dispatch({
|
|
466
|
+
effects: [
|
|
467
|
+
setTobeDecorationsEffect.of(tobeDecorations),
|
|
468
|
+
setTobeButtonDecorationsEffect.of(tobeButtonDecorations)
|
|
469
|
+
]
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
initialize = (src1, src2, language = 'javascript') => {
|
|
474
|
+
if (!this.#asisEditorView || !this.#tobeEditorView) {
|
|
475
|
+
trace.warn('CodeMirror Editors not initialized yet.');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
//console.log(src1, src2, language);
|
|
481
|
+
|
|
482
|
+
this.#isScrollSyncActive = false;
|
|
483
|
+
|
|
484
|
+
let langExtension;
|
|
485
|
+
switch(language) {
|
|
486
|
+
case 'javascript':
|
|
487
|
+
langExtension = javascript();
|
|
488
|
+
break;
|
|
489
|
+
default:
|
|
490
|
+
langExtension = javascript();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
this.#asisEditorView.dispatch({
|
|
494
|
+
changes: { from: 0, to: this.#asisEditorView.state.doc.length, insert: src1 },
|
|
495
|
+
effects: [
|
|
496
|
+
this.#languageCompartment.reconfigure(langExtension)
|
|
497
|
+
]
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
this.#tobeEditorView.dispatch({
|
|
501
|
+
changes: { from: 0, to: this.#tobeEditorView.state.doc.length, insert: src2 },
|
|
502
|
+
effects: [
|
|
503
|
+
this.#languageCompartment.reconfigure(langExtension)
|
|
504
|
+
]
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
requestAnimationFrame(() => {
|
|
508
|
+
const { asisDecorations, tobeDecorations, asisButtonDecorations, tobeButtonDecorations } = this.#applyDiffDecorations(src1, src2);
|
|
509
|
+
|
|
510
|
+
this.#asisEditorView.dispatch({
|
|
511
|
+
effects: [
|
|
512
|
+
setAsisDecorationsEffect.of(asisDecorations),
|
|
513
|
+
setAsisButtonDecorationsEffect.of(asisButtonDecorations)
|
|
514
|
+
]
|
|
515
|
+
});
|
|
516
|
+
this.#tobeEditorView.dispatch({
|
|
517
|
+
effects: [
|
|
518
|
+
setTobeDecorationsEffect.of(tobeDecorations),
|
|
519
|
+
setTobeButtonDecorationsEffect.of(tobeButtonDecorations)
|
|
520
|
+
]
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
requestAnimationFrame(() => {
|
|
524
|
+
this.#isScrollSyncActive = true;
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!customElements.get("nine-diff")) {
|
|
531
|
+
customElements.define("nine-diff", NineDiff);
|
|
532
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class NineDiffContainer extends HTMLElement {
|
|
2
|
+
#asis = "";
|
|
3
|
+
#tobe = "";
|
|
4
|
+
#lang = "javascript";
|
|
5
|
+
#diffView = null;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.attachShadow({ mode: 'open' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 단일 비교 데이터를 설정합니다.
|
|
14
|
+
* @param {Object} data - { asis, tobe, lang }
|
|
15
|
+
*/
|
|
16
|
+
setData = ({ asis, tobe, lang }) => {
|
|
17
|
+
this.#asis = asis || "";
|
|
18
|
+
this.#tobe = tobe || "";
|
|
19
|
+
this.#lang = lang || "javascript";
|
|
20
|
+
|
|
21
|
+
this.render();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 최종 결과물(사용자가 편집한 ASIS 내용)을 가져옵니다.
|
|
25
|
+
getContents = () => {
|
|
26
|
+
return this.#diffView ? this.#diffView.getContents() : this.#asis;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render() {
|
|
30
|
+
this.shadowRoot.innerHTML = `
|
|
31
|
+
<style>
|
|
32
|
+
:host {
|
|
33
|
+
display: block;
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: 100%;
|
|
36
|
+
border: 1px solid #ddd;
|
|
37
|
+
background: #fff;
|
|
38
|
+
}
|
|
39
|
+
.diff-wrapper {
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
44
|
+
nine-diff {
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 100%;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
<div class="diff-wrapper">
|
|
50
|
+
<nine-diff id="single-diff"></nine-diff>
|
|
51
|
+
</div>
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
this.#diffView = this.shadowRoot.querySelector('#single-diff');
|
|
55
|
+
|
|
56
|
+
// 데이터 주입 (DOM이 렌더링된 직후 실행)
|
|
57
|
+
if (this.#diffView) {
|
|
58
|
+
this.#diffView.initialize(this.#asis, this.#tobe, this.#lang);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
customElements.define("nine-diff-container", NineDiffContainer);
|
|
File without changes
|
package/src/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { trace } from '@nine-lab/nine-util';
|
|
2
2
|
import { NineChat } from './components/NineChat.js';
|
|
3
|
+
import { NineDiff } from './components/NineDiff.js';
|
|
4
|
+
import { NineDiffContainer } from './components/NineDiffContainer.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
* Nine-
|
|
7
|
+
* Nine-Mu 엔진 메인 클래스
|
|
6
8
|
*/
|
|
7
|
-
export const
|
|
9
|
+
export const NineMu = {
|
|
8
10
|
version: typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.1.0',
|
|
9
11
|
init: (config) => {
|
|
10
|
-
trace.log("🛠️ Nine-
|
|
12
|
+
trace.log("🛠️ Nine-Mu Engine initialized", config);
|
|
11
13
|
// 향후 커넥터 URL 전역 설정이나 인증 토큰 처리 로직 추가 가능
|
|
12
14
|
}
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
// 기본 export 및 컴포넌트 export
|
|
16
|
-
export default
|
|
17
|
-
export { NineChat };
|
|
18
|
+
export default NineMu;
|
|
19
|
+
export { NineChat, NineDiff, NineDiffContainer };
|