@nine-lab/nine-mu 0.1.263 → 0.1.265
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/package.json
CHANGED
|
@@ -135,6 +135,18 @@ export class NineChatManager {
|
|
|
135
135
|
return res.ok ? await res.json() : [];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
async getUnmappedRoutes(routes) {
|
|
139
|
+
|
|
140
|
+
const res = await api.post("/nine-mu/config/getUnmappedRoutes", {
|
|
141
|
+
routes: routes
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 백엔드 반환 데이터 안전하게 추출 (res.data 구조 확인 필요)
|
|
145
|
+
return res?.data?.unmappedRoutes || res?.unmappedRoutes || [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
138
150
|
addChatHistory(message) {
|
|
139
151
|
|
|
140
152
|
this.#chatHistory.push({ role: 'ai', text: message });
|
|
@@ -156,11 +168,16 @@ export class NineChatManager {
|
|
|
156
168
|
}).join('\n');
|
|
157
169
|
const currentPath = window.location.pathname;
|
|
158
170
|
|
|
171
|
+
const routes = await this.getRoutes();
|
|
172
|
+
const unmappedRoutes = await this.getUnmappedRoutes(routes);
|
|
173
|
+
|
|
159
174
|
// 2. AI 호출 시 history를 함께 실어서 보냅니다.
|
|
160
175
|
return await this.#callTool("system-brain", {
|
|
176
|
+
base_package: this.#owner.config.basePackage, // nine.edu
|
|
161
177
|
user_input : userInput,
|
|
162
178
|
chat_history: formattedHistory,
|
|
163
|
-
routes :
|
|
179
|
+
routes : routes,
|
|
180
|
+
unmapped_routes: unmappedRoutes, // 미개발된 해당 라우트 정보 객체
|
|
164
181
|
current_path : currentPath,
|
|
165
182
|
});
|
|
166
183
|
}
|
|
@@ -189,7 +206,7 @@ export class NineChatManager {
|
|
|
189
206
|
user_input: `다음 미개발 라우트에 대한 소스코드(Controller, Service, MyBatis, React)를 생성해줘.`,
|
|
190
207
|
base_package: this.#owner.config.basePackage, // nine.edu
|
|
191
208
|
//chat_history: "",
|
|
192
|
-
|
|
209
|
+
unmapped_routes: unmappedRoutes, // 미개발된 해당 라우트 정보 객체
|
|
193
210
|
//current_path: route.path, // 현재 생성 기준 패스 명시
|
|
194
211
|
});
|
|
195
212
|
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { trace } from "@nopeer";
|
|
2
|
+
import { nine, api } from '@nine-lab/nine-util';
|
|
3
|
+
import { NineChatManager } from './NineChatManager.js';
|
|
4
|
+
import { NineMuService } from '../services/NineMuService.js';
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
7
|
+
|
|
8
|
+
export class NineChat extends HTMLElement {
|
|
9
|
+
#service = null;
|
|
10
|
+
#packageName;
|
|
11
|
+
#routesPath;
|
|
12
|
+
#connectorUrl;
|
|
13
|
+
#manager;
|
|
14
|
+
#config;
|
|
15
|
+
|
|
16
|
+
#$nineChatMessage;
|
|
17
|
+
|
|
18
|
+
get config() {
|
|
19
|
+
return this.#config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.attachShadow({ mode: 'open' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async connectedCallback() {
|
|
28
|
+
|
|
29
|
+
this.#packageName = this.getAttribute('package-name');
|
|
30
|
+
|
|
31
|
+
// 서비스 초기화
|
|
32
|
+
this.#connectorUrl = this.getAttribute('connector-url') || 'http://localhost:3002';
|
|
33
|
+
|
|
34
|
+
this.#service = new NineMuService(this.#connectorUrl);
|
|
35
|
+
|
|
36
|
+
this.#render();
|
|
37
|
+
this.#initInteractions(); // UI 토글 로직
|
|
38
|
+
this.#initActions(); // 엔터 시 실행 로직
|
|
39
|
+
|
|
40
|
+
const res = await api.post("/nine-mu/config/get", {});
|
|
41
|
+
|
|
42
|
+
this.#config = res;
|
|
43
|
+
this.#routesPath = res?.userDir + "/" + this.#packageName + "/public/prompts/data/routes.json";
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// 경로 데이터 로드
|
|
47
|
+
//this.#routes = await this.#service.fetchRoutes(this.getAttribute('route-url'));
|
|
48
|
+
|
|
49
|
+
this.#manager = new NineChatManager(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- [그룹 1: Interaction] 화면 토글 및 메뉴 클릭 ---
|
|
53
|
+
#initInteractions() {
|
|
54
|
+
const $settings = this.shadowRoot.querySelector('nine-ai-settings');
|
|
55
|
+
const $menuIcons = this.shadowRoot.querySelectorAll("menu .menu-icon");
|
|
56
|
+
|
|
57
|
+
const toggleUI = () => this.classList.toggle("collapse");
|
|
58
|
+
this.shadowRoot.querySelector(".expand-icon").addEventListener("click", toggleUI);
|
|
59
|
+
this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", toggleUI);
|
|
60
|
+
|
|
61
|
+
$menuIcons.forEach(el => {
|
|
62
|
+
el.addEventListener("click", (e) => {
|
|
63
|
+
$menuIcons.forEach(icon => icon.classList.remove("active"));
|
|
64
|
+
const clicked = e.target.closest(".menu-icon");
|
|
65
|
+
clicked.classList.add("active");
|
|
66
|
+
$settings.classList.toggle("expand", clicked.classList.contains('menu-setting'));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.shadowRoot.querySelector(".make-menu")?.addEventListener("click", this.#makeMenuHandler);
|
|
71
|
+
this.shadowRoot.querySelector(".source-gen")?.addEventListener("click", this.#sourceGenHandler);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- [그룹 2: Action] 엔터키 입력 시 서비스 호출 ---
|
|
75
|
+
#initActions() {
|
|
76
|
+
|
|
77
|
+
this.shadowRoot.querySelector('#q').addEventListener('keypress', async (e) => {
|
|
78
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
|
|
81
|
+
const target = e.target;
|
|
82
|
+
|
|
83
|
+
const userInput = target.value.trim();
|
|
84
|
+
if (!userInput) return;
|
|
85
|
+
|
|
86
|
+
target.setAttribute('disabled', 'disabled');
|
|
87
|
+
|
|
88
|
+
// 1. 즉시 UI 반영
|
|
89
|
+
this.#$nineChatMessage.add("me", userInput);
|
|
90
|
+
this.#$nineChatMessage.add("ing", ""); // 로딩 상태
|
|
91
|
+
|
|
92
|
+
// 3. nine.safe로 매니저 로직 실행
|
|
93
|
+
const [result, err] = await nine.safe(this.#manager.handleChatSubmit(target.value));
|
|
94
|
+
|
|
95
|
+
// 2. 입력창 비우기
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
target.value = "";
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
target.removeAttribute('disabled');
|
|
101
|
+
target.focus();
|
|
102
|
+
|
|
103
|
+
if (err) {
|
|
104
|
+
trace.error(err);
|
|
105
|
+
// 에러가 났을 때 기존 로딩 컴포넌트를 지우거나 교체하는 로직이 있다면 연동해 주세요.
|
|
106
|
+
this.#$nineChatMessage.add("ai", err.message || "알 수 없는 오류가 발생했습니다. 다시 시도해주세요.");
|
|
107
|
+
} else {
|
|
108
|
+
trace.log(result);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// 💡 [핵심] MCP 규격의 content[0].text에서 스트링을 추출합니다.
|
|
112
|
+
const rawText = result?.content?.[0]?.text;
|
|
113
|
+
//trace.log(rawText);
|
|
114
|
+
// 텍스트를 JSON 객체로 파싱
|
|
115
|
+
const parsed = JSON.parse(rawText);
|
|
116
|
+
|
|
117
|
+
this.#manager.addChatHistory(parsed.message);
|
|
118
|
+
this.#$nineChatMessage.add("ai", parsed.message);
|
|
119
|
+
|
|
120
|
+
//trace.log(parsed.action);
|
|
121
|
+
//"source-missing"
|
|
122
|
+
//trace.log(parsed.data);
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const asis = await this.#manager.getRoutes();
|
|
126
|
+
|
|
127
|
+
if (parsed?.selected_tool === "source-missing") {
|
|
128
|
+
const hasCreated = parsed?.data.some(item => item.action === 'CREATE');
|
|
129
|
+
const hasUpdated = parsed?.data.some(item => item.action === 'UPDATE');
|
|
130
|
+
const hasDeleted = parsed?.data.some(item => item.action === 'DELETE');
|
|
131
|
+
|
|
132
|
+
if (hasUpdated || hasCreated || hasDeleted) {
|
|
133
|
+
// 진짜 메뉴명 변경이나 이동만 요청한 경우
|
|
134
|
+
nine.alert("메뉴 정리를 위한 라우터 정보 저장 화면으로 이동합니다.").then(res => {
|
|
135
|
+
const tobe = (parsed?.data || [])
|
|
136
|
+
.filter(item => item.action !== 'DELETE')
|
|
137
|
+
.map(({ isNew, action, ...rest }) => rest);
|
|
138
|
+
|
|
139
|
+
this.#showDiff(asis, tobe, "json");
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
// 💡 만약 intent가 EXECUTE_TOOL이거나 NAVIGATE일 때 프론트에서
|
|
148
|
+
// 화면 이동(Routing)이나 추가 액션을 해야 한다면 여기서 parsedData.target_path 등을 활용하면 됩니다!
|
|
149
|
+
|
|
150
|
+
} catch (err) {
|
|
151
|
+
trace.error(err);
|
|
152
|
+
// 만약 AI가 JSON 형식이 아닌 일반 텍스트로 답장했을 경우를 대비한 예외 처리
|
|
153
|
+
const fallbackMessage = result?.content?.[0]?.text || "응답 데이터를 해석하지 못했습니다.";
|
|
154
|
+
this.#$nineChatMessage.add("ai", fallbackMessage);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
#render() {
|
|
163
|
+
const placeholder = this.getAttribute("placeholder") || "나에게 무엇이든 물어봐...";
|
|
164
|
+
const customImport = this.getAttribute("css-path") ? `@import "${this.getAttribute("css-path")}";` : "";
|
|
165
|
+
|
|
166
|
+
this.shadowRoot.innerHTML = `
|
|
167
|
+
<style>
|
|
168
|
+
@import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
|
|
169
|
+
${customImport}
|
|
170
|
+
</style>
|
|
171
|
+
<div class="wrapper">
|
|
172
|
+
<nine-ai-settings></nine-ai-settings>
|
|
173
|
+
<div class="container">
|
|
174
|
+
<div class="head">
|
|
175
|
+
<div class="logo"><span></span></div>
|
|
176
|
+
<div id="status-tag" style="font-size:10px; color:#ff5722; padding:5px;">nine-mu Engine Ready</div>
|
|
177
|
+
</div>
|
|
178
|
+
<nine-chat-message></nine-chat-message>
|
|
179
|
+
<div class="foot">
|
|
180
|
+
<div class="apply-src">
|
|
181
|
+
${['MyBatis', 'Service', 'Controller', 'JavaScript'].map(t => `
|
|
182
|
+
<div>
|
|
183
|
+
<input type="checkbox" id="${t.toLowerCase()}" name="gen_target" value="${t}" checked />
|
|
184
|
+
<label for="${t.toLowerCase()}">${t}</label>
|
|
185
|
+
</div>
|
|
186
|
+
`).join('')}
|
|
187
|
+
</div>
|
|
188
|
+
<textarea id="q" rows="4" placeholder="${placeholder}"></textarea>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="menu">
|
|
192
|
+
<div class="collapse-icon"></div>
|
|
193
|
+
<div class="menu-icon menu-filter"></div>
|
|
194
|
+
<div class="menu-icon menu-general active"></div>
|
|
195
|
+
<div class="menu-icon menu-setting"></div>
|
|
196
|
+
|
|
197
|
+
<div class="menu-icon make-menu"></div>
|
|
198
|
+
<div class="menu-icon source-gen"></div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="expand-icon"></div>
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
//this.#diffPopup = this.shadowRoot.querySelector('nine-diff-popup');
|
|
205
|
+
|
|
206
|
+
this.#$nineChatMessage = this.shadowRoot.querySelector("nine-chat-message");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
addMessage = (who, message) => {
|
|
210
|
+
this.#$nineChatMessage.add(who, message);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#makeMenuHandler = async e => {
|
|
214
|
+
|
|
215
|
+
const prompt = await nine.prompt("메뉴생성시 필요한 AI 프롬프트를 입력하세요.").rgb();
|
|
216
|
+
if (prompt === null || prompt === undefined) return;
|
|
217
|
+
|
|
218
|
+
const res = await api.post(`/nine-mu/source/readFile`, { path: this.#routesPath });
|
|
219
|
+
trace.log(res);
|
|
220
|
+
|
|
221
|
+
if (!res || !res?.contents) return;
|
|
222
|
+
|
|
223
|
+
const jsonRoutes = res?.contents ? JSON.parse(res?.contents) : {};
|
|
224
|
+
trace.log(jsonRoutes);
|
|
225
|
+
|
|
226
|
+
// 1. 미생성 소스 분석 요청
|
|
227
|
+
const res2 = await api.post(`${this.#connectorUrl}/api/source/missing`, {
|
|
228
|
+
routes : jsonRoutes,
|
|
229
|
+
prompt : prompt
|
|
230
|
+
});
|
|
231
|
+
//trace.log("분석 완료. 생성할 태스크:", res2?.tasks);
|
|
232
|
+
|
|
233
|
+
if (res2.success && res2?.tasks) {
|
|
234
|
+
this.#showDiff(jsonRoutes, res2?.tasks, "json");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#sourceGenHandler = async e => {
|
|
239
|
+
const routes = await this.#manager.getRoutes();
|
|
240
|
+
const response = await this.#manager.analyzeUnmappedRoutes(routes);
|
|
241
|
+
trace.log(response);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#showDiff = (asis, tobe, lang) => {
|
|
245
|
+
// 1. 혹시 잔상이 남아있을지 모르니 기존에 생성된 팝업이 있다면 강제 제거
|
|
246
|
+
const oldPopup = this.shadowRoot.querySelector('nine-diff-popup');
|
|
247
|
+
if (oldPopup) {
|
|
248
|
+
oldPopup.remove();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 2. 동적으로 커스텀 엘리먼트 생성
|
|
252
|
+
const $diffPopup = document.createElement('nine-diff-popup');
|
|
253
|
+
|
|
254
|
+
// 3. 래퍼(wrapper) 안이나 shadowRoot 제일 밑바닥에 부착
|
|
255
|
+
const container = this.shadowRoot.querySelector('.wrapper') || this.shadowRoot;
|
|
256
|
+
container.appendChild($diffPopup);
|
|
257
|
+
|
|
258
|
+
// 4. 데이터 주입 및 팝업 실행
|
|
259
|
+
// 💡 팝업이 네이티브 <dialog> 기반이라면 .popup() 내부에서 showModal()이 실행될 겁니다.
|
|
260
|
+
$diffPopup.popup().data(asis, tobe, lang);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!customElements.get('nine-chat')) {
|
|
265
|
+
customElements.define('nine-chat', NineChat);
|
|
266
|
+
}
|