@nine-lab/nine-mu 0.1.333 → 0.1.335

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nine-lab/nine-mu",
3
- "version": "0.1.333",
3
+ "version": "0.1.335",
4
4
  "description": "AI-Driven Full-Stack Code Fabrication Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/nine-mu.umd.js",
@@ -940,4 +940,92 @@
940
940
  /* JS로 내용이 채워지므로 별도의 CSS 애니메이션은 필요 없음 */
941
941
  }
942
942
  }
943
+ }
944
+
945
+ :host(nine-exception-hook) {
946
+ display: block;
947
+ width: 100%;
948
+ height: 100%;
949
+ color: inherit;
950
+ font-size: inherit;
951
+ line-height: inherit;
952
+
953
+ /* 내부 모든 엘리먼트의 여백 계산법을 어드민 기본형에 동기화 */
954
+ .nine-error-container *,
955
+ .nine-error-container *::before,
956
+ .nine-error-container *::after {
957
+ box-sizing: border-box;
958
+ }
959
+
960
+ .nine-error-container {
961
+ padding: 30px;
962
+ margin: 20px;
963
+ background-color: #FFF5F5;
964
+ border: 1px solid #FEB2B2;
965
+ border-radius: 8px;
966
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
967
+ }
968
+
969
+ .nine-error-title {
970
+ color: #C53030;
971
+ margin-top: 0;
972
+ font-size: 18px;
973
+ font-weight: bold;
974
+ }
975
+
976
+ .nine-error-label {
977
+ margin: 10px 0 5px 0;
978
+ font-weight: bold;
979
+ color: #4A5568;
980
+ }
981
+
982
+ .nine-error-log {
983
+ background-color: #1A202C;
984
+ color: #EDF2F7;
985
+ padding: 15px;
986
+ border-radius: 6px;
987
+ overflow-x: auto;
988
+ white-space: pre-wrap;
989
+ font-family: 'Courier New', Courier, monospace;
990
+ font-size: 14px;
991
+ }
992
+
993
+ /* 💡 버튼 커스텀 디자인 세트 */
994
+ .nine-ai-btn {
995
+ background-color: #3182CE;
996
+ color: white;
997
+ padding: 10px 20px;
998
+ font-size: 14px;
999
+ font-weight: bold;
1000
+ border: none;
1001
+ border-radius: 6px;
1002
+ cursor: pointer;
1003
+ transition: background-color 0.2s ease;
1004
+ white-space: nowrap;
1005
+ }
1006
+
1007
+ .nine-ai-btn:hover {
1008
+ background-color: #2B6CB0;
1009
+ }
1010
+
1011
+ .nine-ai-btn:disabled {
1012
+ background-color: #A0AEC0;
1013
+ cursor: not-allowed;
1014
+ }
1015
+
1016
+ .nine-ai-btn.loading {
1017
+ background-color: #4A5568;
1018
+ animation: pulse 1.5s infinite ease-in-out;
1019
+ }
1020
+
1021
+ .nine-error-footer {
1022
+ font-size: 14px;
1023
+ color: #718096;
1024
+ }
1025
+ }
1026
+
1027
+ @keyframes pulse {
1028
+ 0% { opacity: 0.6; }
1029
+ 50% { opacity: 1; }
1030
+ 100% { opacity: 0.6; }
943
1031
  }
@@ -367,7 +367,7 @@ class ChatMessageBody extends HTMLElement
367
367
 
368
368
  #renderer = () => {
369
369
 
370
- const customImport = nine.cssPath ? `@import "${nine.cssPath}/ninePanel.css";` : "";
370
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
371
371
 
372
372
  this.shadowRoot.innerHTML = `
373
373
  <style>
@@ -131,7 +131,8 @@ export class NineChat extends HTMLElement {
131
131
 
132
132
  #render() {
133
133
  const placeholder = this.getAttribute("placeholder") || "나에게 무엇이든 물어봐...";
134
- const customImport = this.getAttribute("css-path") ? `@import "${this.getAttribute("css-path")}";` : "";
134
+
135
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
135
136
 
136
137
  this.shadowRoot.innerHTML = `
137
138
  <style>
@@ -196,7 +196,7 @@ export class NineDiff extends HTMLElement {
196
196
 
197
197
  connectedCallback() {
198
198
 
199
- const customImport = this.getAttribute("css-path") ? `@import "${this.getAttribute("css-path")}";` : "";
199
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
200
200
 
201
201
  this.shadowRoot.innerHTML = `
202
202
  <style>
@@ -161,7 +161,7 @@ export class NineChat extends HTMLElement {
161
161
 
162
162
  #render() {
163
163
  const placeholder = this.getAttribute("placeholder") || "나에게 무엇이든 물어봐...";
164
- const customImport = this.getAttribute("css-path") ? `@import "${this.getAttribute("css-path")}";` : "";
164
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
165
165
 
166
166
  this.shadowRoot.innerHTML = `
167
167
  <style>
@@ -1,94 +1,155 @@
1
+ // src/components/exception/ExceptionHook.js
1
2
  export class ExceptionHook extends HTMLElement {
3
+ // 💡 [핵심] 비공개 인스턴스 필드는 클래스 최상단에 먼저 선언해야 합니다.
4
+ #lastPath = window.location.pathname;
5
+ #hasError = false;
6
+ #cssPath = "/css/nine-exception-hook.css";
7
+
2
8
  constructor() {
3
9
  super();
4
- this.lastPath = window.location.pathname;
5
- this.hasError = false;
6
-
7
- // 💡 깔끔하게 명칭을 싱크한 외부 CSS 경로
8
- this.cssPath = "/css/nine-exception-hook.css";
10
+ // Shadow DOM(open 모드) 활성화
11
+ this.attachShadow({ mode: 'open' });
9
12
  }
10
13
 
11
14
  connectedCallback() {
12
- this.injectStylesheet();
15
+ // 기본 스켈레톤 마크업 렌더링
16
+ this.#renderer();
13
17
 
14
- // 런타임 에러 비동기 거부(Promise Rejection)에 갈고리(Hook)를 겁니다.
15
- window.addEventListener('error', this.handleRuntimeError.bind(this), true);
16
- window.addEventListener('unhandledrejection', this.handlePromiseError.bind(this), true);
17
- window.addEventListener('popstate', this.handleLocationChange.bind(this));
18
- document.addEventListener('click', this.handleLocationChange.bind(this), true);
18
+ // 정적 외부 스타일시트 링크 안전하게 결합
19
+ this.#injectShadowStylesheet();
20
+
21
+ window.addEventListener('error', this.#handleRuntimeError, true);
22
+ window.addEventListener('unhandledrejection', this.#handlePromiseError, true);
23
+ window.addEventListener('popstate', this.#handleLocationChange);
24
+ document.addEventListener('click', this.#handleLocationChange, true);
19
25
  }
20
26
 
21
27
  disconnectedCallback() {
22
- window.removeEventListener('error', this.handleRuntimeError);
23
- window.removeEventListener('unhandledrejection', this.handlePromiseError);
24
- window.removeEventListener('popstate', this.handleLocationChange);
25
- document.removeEventListener('click', this.handleLocationChange, true);
28
+ window.removeEventListener('error', this.#handleRuntimeError, true);
29
+ window.removeEventListener('unhandledrejection', this.#handlePromiseError, true);
30
+ window.removeEventListener('popstate', this.#handleLocationChange);
31
+ document.removeEventListener('click', this.#handleLocationChange, true);
32
+ }
33
+
34
+ #renderer = () => {
35
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
36
+
37
+ this.shadowRoot.innerHTML = `
38
+ <style>
39
+ @import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
40
+ ${customImport}
41
+ </style>
42
+
43
+ <div class="nine-hook-core">
44
+ <slot></slot>
45
+ </div>
46
+ <div class="nine-exception-board"></div>
47
+ `;
26
48
  }
27
49
 
28
- injectStylesheet() {
29
- if (document.querySelector(`link[href="${this.cssPath}"]`)) return;
50
+ #injectShadowStylesheet() {
51
+ // 💡 내부 필드 호출 시에도 모두 this.#형식으로 매핑합니다.
52
+ if (this.shadowRoot.querySelector(`link[href="${this.#cssPath}"]`)) return;
30
53
 
31
54
  const link = document.createElement('link');
32
55
  link.rel = 'stylesheet';
33
- link.href = this.cssPath;
34
- document.head.appendChild(link);
56
+ link.href = this.#cssPath;
57
+ this.shadowRoot.appendChild(link);
35
58
  }
36
59
 
37
- handleRuntimeError(event) {
60
+ #handleRuntimeError = (event) => {
61
+ if (this.#hasError) return;
62
+
38
63
  if (this.contains(event.target) || event.error) {
39
- this.renderError(event.error?.message || event.message);
64
+ this.#renderError(event.error?.message || event.message, event.error?.stack);
40
65
  }
41
66
  }
42
67
 
43
- handlePromiseError(event) {
44
- this.renderError(event.reason?.message || String(event.reason));
68
+ #handlePromiseError = (event) => {
69
+ if (this.#hasError) return;
70
+ this.#renderError(event.reason?.message || String(event.reason), event.reason?.stack);
45
71
  }
46
72
 
47
- handleLocationChange() {
73
+ #handleLocationChange = () => {
48
74
  setTimeout(() => {
49
- // 주소가 바뀌면 훅(Hook)을 일시적으로 풀고 화면을 부드럽게 복구
50
- if (this.hasError && this.lastPath !== window.location.pathname) {
51
- console.log("✨ [nine-exception-hook] 예외 해제 및 렌더링 정상 복구");
52
- this.lastPath = window.location.pathname;
53
- this.hasError = false;
75
+ if (this.#hasError && this.#lastPath !== window.location.pathname) {
76
+ console.log("✨ [nine-exception-hook] Shadow DOM 내부 예외 청소 및 정상 복구");
77
+ this.#lastPath = window.location.pathname;
78
+ this.#hasError = false;
54
79
 
55
- const errorBox = this.querySelector('.nine-error-container');
56
- if (errorBox) errorBox.remove();
80
+ const exceptionBoard = this.shadowRoot.querySelector('.nine-exception-board');
81
+ if (exceptionBoard) exceptionBoard.innerHTML = '';
57
82
 
58
- Array.from(this.children).forEach(child => {
59
- if (child.tagName !== 'LINK') child.style.display = '';
60
- });
83
+ const coreContainer = this.shadowRoot.querySelector('.nine-hook-core');
84
+ if (coreContainer) coreContainer.style.display = '';
61
85
  }
62
86
  }, 10);
63
87
  }
64
88
 
65
- renderError(errorMessage) {
66
- if (this.hasError) return;
67
- this.hasError = true;
89
+ #sendToAiAction = async (errorMessage, errorStack) => {
90
+ const btn = this.shadowRoot.querySelector('.nine-ai-btn');
91
+ const footer = this.shadowRoot.querySelector('.nine-error-footer');
92
+ if (!btn) return;
93
+
94
+ btn.disabled = true;
95
+ btn.innerText = "⚡ AI 관제탑에 소스 분석 요청 중...";
96
+ btn.classList.add('loading');
97
+
98
+ try {
99
+ if (window.triggerAutoRecovery) {
100
+ await window.triggerAutoRecovery({
101
+ message: errorMessage,
102
+ stack: errorStack || "No stack trace available",
103
+ path: window.location.pathname
104
+ });
68
105
 
69
- if (window.triggerAutoRecovery) {
70
- window.triggerAutoRecovery(errorMessage);
106
+ footer.innerHTML = `✅ <b>AI 관제탑 접수 완료:</b> 분석을 바탕으로 소스 코드를 교정하고 있습니다. 잠시만 기다려주세요.`;
107
+ btn.innerText = "🚀 전송 완료";
108
+ } else {
109
+ throw new Error("window.triggerAutoRecovery 관제탑 엔진이 로드되지 않았습니다.");
110
+ }
111
+ } catch (err) {
112
+ console.error("❌ AI 전송 실패:", err);
113
+ btn.disabled = false;
114
+ btn.innerText = "❌ 재전송 시도";
115
+ footer.innerHTML = `<span style="color: #E53E3E;">⚠️ 전송 실패: ${err.message}</span>`;
71
116
  }
117
+ }
118
+
119
+ #renderError(errorMessage, errorStack) {
120
+ if (this.#hasError) return;
121
+ this.#hasError = true;
122
+
123
+ const coreContainer = this.shadowRoot.querySelector('.nine-hook-core');
124
+ if (coreContainer) coreContainer.style.display = 'none';
72
125
 
73
- Array.from(this.children).forEach(child => {
74
- if (child.tagName !== 'LINK') child.style.display = 'none';
75
- });
126
+ const exceptionBoard = this.shadowRoot.querySelector('.nine-exception-board');
127
+ if (!exceptionBoard) return;
76
128
 
77
129
  const errorHTML = `
78
130
  <div class="nine-error-container">
79
131
  <h3 class="nine-error-title">⚠️ 시스템 예외 감지 (${this.tagName.toLowerCase()})</h3>
80
132
  <p class="nine-error-label">[에러 원인 로그]:</p>
81
133
  <pre class="nine-error-log">${errorMessage}</pre>
82
- <div class="nine-error-footer">
83
- 🔄 에러 로그가 관제탑으로 자동 피드백되었습니다. AI가 소스 코드를 재수정하는 동안 잠시만 기다려주세요...
134
+
135
+ <div class="nine-action-area" style="margin-top: 20px; display: flex; align-items: center; gap: 15px;">
136
+ <button class="nine-ai-btn">🤖 AI에게 소스코드 수정 요청</button>
137
+ <div class="nine-error-footer">
138
+ 버튼을 누르면 AI가 로그 및 컴포넌트 맵을 역추적하여 수정을 시작합니다.
139
+ </div>
84
140
  </div>
85
141
  </div>
86
142
  `;
87
- this.insertAdjacentHTML('beforeend', errorHTML);
143
+
144
+ exceptionBoard.innerHTML = errorHTML;
145
+
146
+ const aiBtn = this.shadowRoot.querySelector('.nine-ai-btn');
147
+ if (aiBtn) {
148
+ aiBtn.addEventListener('click', () => this.#sendToAiAction(errorMessage, errorStack));
149
+ }
88
150
  }
89
151
  }
90
152
 
91
- // 💡 전역 브라우저 템플릿 태그로 공식 등록
92
153
  if (!customElements.get('nine-exception-hook')) {
93
154
  customElements.define('nine-exception-hook', ExceptionHook);
94
155
  }