@nine-lab/nine-mu 0.1.396 → 0.1.398

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.396",
3
+ "version": "0.1.398",
4
4
  "description": "AI-Driven Full-Stack Code Fabrication Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/nine-mu.umd.js",
@@ -1028,4 +1028,127 @@
1028
1028
  0% { opacity: 0.6; }
1029
1029
  50% { opacity: 1; }
1030
1030
  100% { opacity: 0.6; }
1031
+ }
1032
+
1033
+ :host(nine-natual-query) {
1034
+ .input-wrapper {
1035
+ position: relative;
1036
+ width: 100%;
1037
+ }
1038
+
1039
+ input {
1040
+ width: 100%;
1041
+ padding: 8px 16px; /* 하단 padding을 충분히 주어 워터마크 공간 확보 */
1042
+ font-size: 14px;
1043
+ border: 1px solid #d1d5da;
1044
+ border-radius: 6px;
1045
+ box-sizing: border-box;
1046
+ transition: border-color 0.2s, box-shadow 0.2s;
1047
+ }
1048
+
1049
+ input::placeholder {
1050
+ color: #d1d5da; /* 아주 연한 회색 */
1051
+ opacity: 1; /* 파이어폭스 대응 */
1052
+ }
1053
+
1054
+ input:focus {
1055
+ border-color: green;
1056
+ --box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
1057
+ outline: none;
1058
+ }
1059
+
1060
+ /* 핵심: 워터마크 스타일 */
1061
+ .watermark-link {
1062
+ position: absolute;
1063
+ right: 8px;
1064
+ bottom: 2px;
1065
+ font-size: 11px;
1066
+ color: #999;
1067
+ text-decoration: none;
1068
+ opacity: 0.7;
1069
+ z-index: 5;
1070
+ font-weight: 500;
1071
+ letter-spacing: -0.2px;
1072
+ }
1073
+
1074
+ .watermark-link:hover {
1075
+ color: #0366d6;
1076
+ opacity: 1;
1077
+ text-decoration: underline;
1078
+ }
1079
+ }
1080
+
1081
+ :host(nine-natual-query-result) {
1082
+ display: block;
1083
+ width: 100%;
1084
+ height: 100%;
1085
+ min-height: 200px; /* 초기 문구가 보일 만큼 최소 높이 확보 */
1086
+ border: 1px solid #e1e4e8;
1087
+ border-radius: 8px;
1088
+ overflow: hidden;
1089
+ box-sizing: border-box;
1090
+
1091
+ nine-grid {
1092
+ width: 100%;
1093
+ height: 100%;
1094
+ }
1095
+
1096
+ .result-wrapper {
1097
+ width: 100%;
1098
+ height: 100%;
1099
+ display: flex;
1100
+ flex-direction: column;
1101
+ position: relative;
1102
+ background: #fcfcfc;
1103
+ padding: 16px;
1104
+ box-sizing: border-box;
1105
+ }
1106
+
1107
+ /* 1. 기본 상태: 그리드 컨테이너는 보이고, 안내 컨테이너는 숨김 */
1108
+ #grid-container {
1109
+ flex: 1;
1110
+ width: 100%;
1111
+ height: 100%;
1112
+ display: block;
1113
+ }
1114
+
1115
+ #no-result-container {
1116
+ display: none;
1117
+ flex: 1;
1118
+ width: 100%;
1119
+ height: 100%;
1120
+ }
1121
+
1122
+ /* 2. 데이터가 없을 때 (.empty 클래스가 붙었을 때) 제어 */
1123
+ .result-wrapper.empty #grid-container {
1124
+ display: none;
1125
+ }
1126
+
1127
+ .result-wrapper.empty #no-result-container {
1128
+ display: flex; /* 보임 처리 */
1129
+ flex-direction: column;
1130
+ align-items: center;
1131
+ justify-content: center;
1132
+ text-align: center;
1133
+ }
1134
+
1135
+ /* 3. 안내 문구 스타일 디테일 */
1136
+ .no-result .main-message {
1137
+ font-size: 1.2rem;
1138
+ font-weight: 700;
1139
+ color: #2d3748;
1140
+ margin-bottom: 8px;
1141
+ }
1142
+
1143
+ .no-result .sub-message {
1144
+ font-size: 0.9rem;
1145
+ color: #718096;
1146
+ line-height: 1.5;
1147
+ }
1148
+
1149
+ /* 내부 그리드 라이브러리 높이 보정 */
1150
+ nine-grid {
1151
+ width: 100%;
1152
+ height: 100%;
1153
+ }
1031
1154
  }
@@ -0,0 +1,145 @@
1
+ import { trace } from '@nopeer';
2
+ import { api } from '@nine-lab/nine-util';
3
+ import { TipPopup } from '@nine-lab/nine-ai';
4
+
5
+ export class NineNatualQuery extends HTMLElement {
6
+
7
+ #schema = null;
8
+ #connectorUrl = '';
9
+ #aiProcessor = null;
10
+ #tipPopup;
11
+
12
+ constructor() {
13
+ super();
14
+ this.attachShadow({ mode: 'open' });
15
+ }
16
+
17
+ async connectedCallback() {
18
+ // 속성에서 커넥터 주소 읽기 (기본값 설정 가능)
19
+ this.#connectorUrl = this.getAttribute('connector-url') || 'http://localhost:3001';
20
+
21
+ this.#render();
22
+ }
23
+
24
+
25
+ /**
26
+ * 서버(Connector)에 생성된 SQL 실행 요청
27
+ */
28
+ async #runQuery(sql) {
29
+ try {
30
+ trace.log("🚀 쿼리 실행 요청:", sql);
31
+
32
+ const response = await fetch(`${this.#connectorUrl}/api/query`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ sql: sql })
36
+ });
37
+
38
+ trace.log(response);
39
+
40
+ if (!response.ok) throw new Error('쿼리 실행 실패');
41
+
42
+ const result = await response.json();
43
+
44
+ if (result.success) {
45
+ trace.log("✅ 쿼리 실행 결과:", result.data);
46
+
47
+ return result.data;
48
+ // TODO: 결과를 UI(표 등)에 출력하는 로직 추가
49
+ } else {
50
+ throw new Error(result.error || '쿼리 실행 중 오류 발생');
51
+ }
52
+ } catch (error) {
53
+ trace.error("❌ 쿼리 실행 실패:", error.message);
54
+ }
55
+ }
56
+
57
+
58
+ #render() {
59
+
60
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
61
+
62
+ this.shadowRoot.innerHTML = `
63
+ <style>
64
+ @import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
65
+ ${customImport}
66
+ </style>
67
+
68
+ <div class="input-wrapper">
69
+ <input type="text" placeholder="자연어로 질문을 입력하고 엔터를 누르세요.">
70
+ <!-- CSS로 제어되는 워터마크 -->
71
+ <a href="https://www.nine-lab.com" target="_blank" class="watermark-link">powered by nine-lab.com</a>
72
+ </div>
73
+ `;
74
+
75
+ // 2. input 요소를 찾아 엔터 이벤트 바인딩
76
+ const input = this.shadowRoot.querySelector('input');
77
+
78
+
79
+ if (customElements.get("nine-ai-tip-popup")) {
80
+ this.#tipPopup = document.createElement('nine-ai-tip-popup');
81
+ this.shadowRoot.appendChild(this.#tipPopup);
82
+ } else {
83
+ trace.error("nine-ai-tip-popup 컴포넌트를 찾을 수 없습니다.");
84
+ }
85
+
86
+ input.addEventListener('keypress', (e) => {
87
+ // 엔터 키인지 확인 (Enter 키의 키코드는 'Enter'입니다)
88
+ if (e.key === 'Enter') {
89
+ const question = e.target.value.trim();
90
+
91
+ if (question) {
92
+ this.#handleQuestion(question); // 질문 처리 로직 호출
93
+ e.target.value = ''; // 입력창 비우기 (선택 사항)
94
+ }
95
+ }
96
+ });
97
+ }
98
+
99
+
100
+ async #handleQuestion(question) {
101
+ try {
102
+ // 1. 대기 팝업 표시
103
+ await this.#tipPopup?.popup();
104
+
105
+ // 2. 서버의 자연어 질의 API 호출
106
+ // 기존의 filterTables, generateSql 과정이 서버 하나로 통합됨
107
+ const response = await api.post(`/nine-mu/query/executeNaturalLanguageQuery`, {
108
+ userInput: question
109
+ });
110
+
111
+ trace.log(response);
112
+ /**
113
+ const result = await response.json();
114
+
115
+ if (!result.success) {
116
+ // 서버에서 실행 불가능하거나 에러가 난 경우
117
+ nine.alert(result.error || "질의를 처리할 수 없습니다.").rgb().shake();
118
+ return;
119
+ }
120
+
121
+ // 3. 결과 로그 출력 (서버가 보내준 SQL과 설명)
122
+ trace.log("생성된 SQL:", result.sql);
123
+ trace.log("AI 설명:", result.explanation);
124
+
125
+ // 4. 결과 데이터 렌더링
126
+ if (result.data) {
127
+ const el = document.querySelector("nine-query-result");
128
+ if (el) {
129
+ el.redraw(result.data); // 서버가 이미 실행해서 보낸 데이터를 바로 사용
130
+ }
131
+ }
132
+ */
133
+ } catch (error) {
134
+ trace.error("서버 통신 실패:", error);
135
+ nine.alert("서버와 연결할 수 없습니다.").rgb().shake();
136
+ } finally {
137
+ // 5. 대기 팝업 닫기
138
+ this.#tipPopup?.close();
139
+ }
140
+ }
141
+ }
142
+
143
+ if (!customElements.get("nine-natual-query")) {
144
+ customElements.define("nine-natual-query", NineNatualQuery);
145
+ }
@@ -0,0 +1,145 @@
1
+ import { trace } from '@nopeer';
2
+ import { nine } from '@nine-lab/nine-util';
3
+ //import ninegrid from "ninegrid2";
4
+
5
+ export class NineNatualQueryResult extends HTMLElement {
6
+ // 내부 상태 및 상수 캡슐화
7
+ #data = [];
8
+ #wrapper = null;
9
+ #gridContainer = null;
10
+
11
+ constructor() {
12
+ super();
13
+ this.attachShadow({ mode: 'open' });
14
+ }
15
+
16
+ connectedCallback() {
17
+ this.#render(); // 외부 접근 불가
18
+ }
19
+
20
+ /**
21
+ * 외부 공개 메서드: 데이터 주입 및 그리드 갱신
22
+ */
23
+ redraw(data) {
24
+ this.#data = data || [];
25
+ this.#updateGrid();
26
+ }
27
+
28
+ /**
29
+ * 내부 그리드 업데이트 로직 (Private)
30
+ */
31
+ #updateGrid() {
32
+ // 캐싱된 엘리먼트 참조 확인
33
+ this.#wrapper = this.shadowRoot.querySelector('.result-wrapper');
34
+ this.#gridContainer = this.shadowRoot.querySelector('#grid-container');
35
+
36
+ if (!this.#wrapper || !this.#gridContainer) return;
37
+
38
+ // 1. 데이터 부재 시 상태 제어
39
+ if (this.#data.length === 0) {
40
+ //this.#wrapper.classList.add('empty');
41
+ //this.#gridContainer.innerHTML = '';
42
+ nine.alert("일치하는 검색 결과가 없습니다.").rgb().shake();
43
+ return;
44
+ }
45
+
46
+ // 2. 데이터 존재 시 상태 제어 및 그리드 생성
47
+ this.#wrapper.classList.remove('empty');
48
+ this.#gridContainer.innerHTML = '';
49
+
50
+ const keys = Object.keys(this.#data[0]);
51
+ const gridHtml = this.#generateGridHtml(keys); // HTML 생성 로직 분리
52
+
53
+ this.#gridContainer.innerHTML = gridHtml;
54
+
55
+ // 3. 엔진 초기화 대기 및 데이터 바인딩
56
+ this.#bindGridData();
57
+ }
58
+
59
+ /**
60
+ * nine-grid HTML 구조 생성 (Private)
61
+ */
62
+ #generateGridHtml(keys) {
63
+ return `
64
+ <nine-grid
65
+ caption="검색 결과"
66
+ select-type="row"
67
+ show-title-bar="true"
68
+ show-menu-icon="true"
69
+ show-status-bar="true"
70
+ enable-fixed-col="true"
71
+ row-resizable="false"
72
+ col-movable="true"
73
+ >
74
+ <table>
75
+ <colgroup>
76
+ <col width="50" fixed="left" />
77
+ ${keys.map(() => `<col width="150"/>`).join('')}
78
+ </colgroup>
79
+ <thead>
80
+ <tr>
81
+ <th>No.</th>
82
+ ${keys.map(key => `<th>${key.toUpperCase()}</th>`).join('')}
83
+ </tr>
84
+ </thead>
85
+ <tbody>
86
+ <tr>
87
+ <th><ng-row-indicator/></th>
88
+ ${keys.map(key => `<td data-bind="${key}"></td>`).join('')}
89
+ </tr>
90
+ </tbody>
91
+ </table>
92
+ </nine-grid>
93
+ `;
94
+ }
95
+
96
+ /**
97
+ * ninegrid2 라이브러리 엔진 바인딩 (Private)
98
+ */
99
+ #bindGridData() {
100
+ const newGrid = this.#gridContainer.querySelector('nine-grid');
101
+ const attemptBind = () => {
102
+ if (newGrid && newGrid.data) {
103
+ newGrid.data.source = this.#data;
104
+ } else {
105
+ setTimeout(attemptBind, 10);
106
+ }
107
+ };
108
+ attemptBind();
109
+ }
110
+
111
+ /**
112
+ * 초기 렌더링 및 스타일 주입 (Private)
113
+ */
114
+ /**
115
+ * 초기 렌더링 및 스타일 주입 (Private)
116
+ */
117
+ #render() {
118
+
119
+ const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
120
+
121
+ this.shadowRoot.innerHTML = `
122
+ <style>
123
+ @import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
124
+ ${customImport}
125
+ </style>
126
+
127
+ <div class="result-wrapper empty">
128
+ <div id="grid-container"></div>
129
+
130
+ <!-- 깔끔한 안내 영역 -->
131
+ <div id="no-result-container" class="no-result">
132
+ <div class="main-message">Ready to Query with Nine AI</div>
133
+ <div class="sub-message">
134
+ 검색창에 궁금한 데이터를 자연어로 입력해보세요.<br>
135
+ AI가 즉시 분석하여 최적화된 데이타를 조회합니다.
136
+ </div>
137
+ </div>
138
+ </div>
139
+ `;
140
+ }
141
+ }
142
+
143
+ if (!customElements.get("nine-natual-query-result")) {
144
+ customElements.define("nine-natual-query-result", NineNatualQueryResult);
145
+ }
@@ -51,7 +51,7 @@ const createDynamicRoutes = (menuData, projectViews, Custom404) => {
51
51
 
52
52
  if (menuData && Array.isArray(menuData)) {
53
53
  menuData.forEach((menu) => {
54
- if (menu.level === 2 && menu.path) {
54
+ if (menu.path) {
55
55
  const cleanPath = menu.path.replace(/^\/+|\/+$/g, "");
56
56
  const pathParts = cleanPath.split("/");
57
57
  const len = pathParts.length;
package/src/index.js CHANGED
@@ -4,6 +4,8 @@ import { NineDiff } from './components/NineDiff.js';
4
4
  import { NineDiffPopup } from './components/NineDiffPopup.js';
5
5
  import { NineMenuDiffPopup } from './components/NineMenuDiffPopup.js';
6
6
  import './components/ChatMessage.js';
7
+ import './components/ai/NineNatualQuery.js';
8
+ import './components/ai/NineNatualQueryResult.js';
7
9
  import { NineHook, useScreen } from './components/hook/NineHook.js';
8
10
 
9
11
  /**