ide-assi 0.528.0 → 0.530.0
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/bundle.cjs.js +340 -17
- package/dist/bundle.esm.js +340 -17
- package/dist/components/ideLoadingTips.js +259 -0
- package/dist/components/ideTipPopup.js +84 -19
- package/package.json +1 -1
- package/src/components/ideLoadingTips.js +259 -0
- package/src/components/ideTipPopup.js +84 -19
package/dist/bundle.esm.js
CHANGED
|
@@ -202894,43 +202894,366 @@ class ideAssiSettings extends HTMLElement
|
|
|
202894
202894
|
|
|
202895
202895
|
customElements.define("ide-assi-settings", ideAssiSettings);
|
|
202896
202896
|
|
|
202897
|
-
|
|
202898
|
-
|
|
202897
|
+
// aiLoadingTips.js
|
|
202898
|
+
// This file contains the full source code for the nx-ai-loading-tips web component.
|
|
202899
|
+
|
|
202900
|
+
|
|
202901
|
+
class IdeLoadingTips extends HTMLElement {
|
|
202902
|
+
#tips = []; // Stores tip objects {text: string, images: string[]}
|
|
202903
|
+
#currentTipIndex = 0; // Index of the currently displayed tip
|
|
202904
|
+
#currentImageIndex = 0; // Index of the currently displayed image for the current tip
|
|
202905
|
+
#tipIntervalId = null; // Interval ID for cycling through tips
|
|
202906
|
+
#imageIntervalId = null; // Interval ID for cycling through images within a tip
|
|
202907
|
+
#tipDisplayDuration = 5000; // How long each tip is displayed (5 seconds)
|
|
202908
|
+
#imageDisplayDuration = 2000; // How long each image within a tip is displayed (2 seconds)
|
|
202909
|
+
|
|
202910
|
+
// DOM element references (cached for performance)
|
|
202911
|
+
tipElement = null;
|
|
202912
|
+
loadingAnimation = null;
|
|
202913
|
+
tipImageElement = null;
|
|
202914
|
+
tipImageContainer = null;
|
|
202915
|
+
|
|
202899
202916
|
constructor() {
|
|
202900
|
-
|
|
202901
202917
|
super();
|
|
202902
202918
|
this.attachShadow({ mode: 'open' });
|
|
202903
202919
|
}
|
|
202904
202920
|
|
|
202905
202921
|
connectedCallback() {
|
|
202906
|
-
|
|
202922
|
+
// Set up the initial structure of the component in its Shadow DOM
|
|
202907
202923
|
this.shadowRoot.innerHTML = `
|
|
202908
|
-
|
|
202909
|
-
|
|
202910
|
-
|
|
202911
|
-
|
|
202924
|
+
<style>
|
|
202925
|
+
/* Import ninegrid's base AI CSS */
|
|
202926
|
+
@import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/ai.css";
|
|
202927
|
+
/* Import custom CSS specific to this component (adjust path as needed) */
|
|
202928
|
+
${ninegrid.getCustomPath(this, "ai.css")}
|
|
202912
202929
|
|
|
202913
|
-
|
|
202914
|
-
|
|
202915
|
-
|
|
202916
|
-
|
|
202917
|
-
|
|
202918
|
-
|
|
202919
|
-
|
|
202930
|
+
/* Component-specific styles */
|
|
202931
|
+
:host {
|
|
202932
|
+
display: flex;
|
|
202933
|
+
justify-content: flex-start;
|
|
202934
|
+
padding: 5px;
|
|
202935
|
+
flex-direction: column;
|
|
202936
|
+
}
|
|
202937
|
+
|
|
202938
|
+
.chat-message.loading-tips-message {
|
|
202939
|
+
max-width: 80%;
|
|
202940
|
+
border-radius: 8px;
|
|
202941
|
+
font-size: 14px;
|
|
202942
|
+
background-color: #fff;
|
|
202943
|
+
color: black;
|
|
202944
|
+
align-self: flex-start;
|
|
202945
|
+
text-align: left;
|
|
202946
|
+
position: relative;
|
|
202947
|
+
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15);
|
|
202948
|
+
padding: 12px 16px;
|
|
202949
|
+
display: flex;
|
|
202950
|
+
flex-direction: column;
|
|
202951
|
+
align-items: flex-start;
|
|
202952
|
+
padding-bottom: 8px; /* Adjusted for image container */
|
|
202953
|
+
}
|
|
202954
|
+
|
|
202955
|
+
.loading-animation {
|
|
202956
|
+
width: 20px;
|
|
202957
|
+
height: 20px;
|
|
202958
|
+
border: 3px solid #f3f3f3;
|
|
202959
|
+
border-top: 3px solid #3498db;
|
|
202960
|
+
border-radius: 50%;
|
|
202961
|
+
animation: spin 1s linear infinite;
|
|
202962
|
+
margin-bottom: 8px;
|
|
202963
|
+
display: none; /* Controlled by JS */
|
|
202964
|
+
}
|
|
202965
|
+
|
|
202966
|
+
.tip-content {
|
|
202967
|
+
width: 100%;
|
|
202968
|
+
}
|
|
202969
|
+
|
|
202970
|
+
.tip-label {
|
|
202971
|
+
font-size: 0.85em;
|
|
202972
|
+
color: #666;
|
|
202973
|
+
margin: 0 0 5px 0;
|
|
202974
|
+
font-weight: bold;
|
|
202975
|
+
}
|
|
202976
|
+
|
|
202977
|
+
.current-tip {
|
|
202978
|
+
font-size: 1em;
|
|
202979
|
+
margin: 0;
|
|
202980
|
+
color: #333;
|
|
202981
|
+
line-height: 1.5;
|
|
202982
|
+
margin-bottom: 10px; /* Space between text and image */
|
|
202983
|
+
}
|
|
202984
|
+
|
|
202985
|
+
.tip-image-container {
|
|
202986
|
+
width: 100%;
|
|
202987
|
+
max-height: 150px; /* Max height for the image area */
|
|
202988
|
+
overflow: hidden;
|
|
202989
|
+
display: flex;
|
|
202990
|
+
justify-content: center;
|
|
202991
|
+
align-items: center;
|
|
202992
|
+
background-color: #f0f0f0; /* Optional background for image area */
|
|
202993
|
+
border-radius: 4px;
|
|
202994
|
+
margin-top: 5px;
|
|
202995
|
+
display: none; /* Controlled by JS */
|
|
202996
|
+
}
|
|
202997
|
+
|
|
202998
|
+
.tip-image {
|
|
202999
|
+
max-width: 100%;
|
|
203000
|
+
max-height: 100%;
|
|
203001
|
+
object-fit: contain; /* Ensures image fits without cropping, maintaining aspect ratio */
|
|
203002
|
+
border-radius: 4px;
|
|
203003
|
+
display: none; /* Controlled by JS */
|
|
203004
|
+
}
|
|
203005
|
+
|
|
203006
|
+
@keyframes spin {
|
|
203007
|
+
0% { transform: rotate(0deg); }
|
|
203008
|
+
100% { transform: rotate(360deg); }
|
|
203009
|
+
}
|
|
203010
|
+
</style>
|
|
203011
|
+
<div class="chat-message loading-tips-message">
|
|
203012
|
+
<div class="loading-animation"></div>
|
|
203013
|
+
<div class="tip-content">
|
|
203014
|
+
<p class="tip-label">오늘의 AI 팁:</p>
|
|
203015
|
+
<p class="current-tip"></p>
|
|
203016
|
+
<div class="tip-image-container">
|
|
203017
|
+
<img class="tip-image" src="" alt="팁 관련 이미지">
|
|
203018
|
+
</div>
|
|
203019
|
+
</div>
|
|
203020
|
+
</div>
|
|
203021
|
+
`;
|
|
203022
|
+
// Cache DOM element references
|
|
203023
|
+
this.tipElement = this.shadowRoot.querySelector('.current-tip');
|
|
203024
|
+
this.loadingAnimation = this.shadowRoot.querySelector('.loading-animation');
|
|
203025
|
+
this.tipImageElement = this.shadowRoot.querySelector('.tip-image');
|
|
203026
|
+
this.tipImageContainer = this.shadowRoot.querySelector('.tip-image-container');
|
|
203027
|
+
}
|
|
203028
|
+
|
|
203029
|
+
disconnectedCallback() {
|
|
203030
|
+
// Clean up all intervals when the component is removed from the DOM
|
|
203031
|
+
this.stopTips();
|
|
203032
|
+
}
|
|
203033
|
+
|
|
203034
|
+
/**
|
|
203035
|
+
* Loads tips asynchronously from a JSON URL and starts displaying them.
|
|
203036
|
+
* @param {string} tipsUrl - The URL of the JSON file containing tip data.
|
|
203037
|
+
* @param {string} tipCategory - The key in the JSON file's data that corresponds to the desired tip array (e.g., 'componentTips').
|
|
203038
|
+
*/
|
|
203039
|
+
async startTips(tipsUrl, tipCategory) {
|
|
203040
|
+
if (!tipsUrl || !tipCategory) {
|
|
203041
|
+
console.error("Tips URL or category not provided for aiLoadingTips.");
|
|
203042
|
+
return;
|
|
203043
|
+
}
|
|
203044
|
+
|
|
203045
|
+
try {
|
|
203046
|
+
const response = await fetch(tipsUrl);
|
|
203047
|
+
if (!response.ok) {
|
|
203048
|
+
throw new Error(`Failed to fetch tips: ${response.statusText}`);
|
|
203049
|
+
}
|
|
203050
|
+
const data = await response.json();
|
|
203051
|
+
const tips = data[tipCategory];
|
|
203052
|
+
|
|
203053
|
+
if (!tips || tips.length === 0) {
|
|
203054
|
+
console.warn(`No tips found for category '${tipCategory}' in ${tipsUrl}.`);
|
|
203055
|
+
return;
|
|
203056
|
+
}
|
|
203057
|
+
|
|
203058
|
+
this.#tips = tips;
|
|
203059
|
+
this.#currentTipIndex = 0;
|
|
203060
|
+
this.stopTips(); // Stop any existing intervals before starting new ones
|
|
203061
|
+
|
|
203062
|
+
this.showCurrentTip(); // Display the first tip immediately
|
|
203063
|
+
|
|
203064
|
+
// Start interval for cycling through tips
|
|
203065
|
+
this.#tipIntervalId = setInterval(() => {
|
|
203066
|
+
this.showNextTip();
|
|
203067
|
+
}, this.#tipDisplayDuration);
|
|
203068
|
+
|
|
203069
|
+
this.loadingAnimation.style.display = 'block'; // Show the loading spinner
|
|
203070
|
+
} catch (error) {
|
|
203071
|
+
console.error("Error loading tips:", error);
|
|
203072
|
+
this.stopTips(); // Stop intervals and hide animations on error
|
|
203073
|
+
if (this.tipElement) {
|
|
203074
|
+
this.tipElement.textContent = "팁을 불러오는 중 오류가 발생했습니다.";
|
|
203075
|
+
}
|
|
203076
|
+
}
|
|
203077
|
+
}
|
|
203078
|
+
|
|
203079
|
+
/**
|
|
203080
|
+
* Stops all tip and image display intervals and hides related UI elements.
|
|
203081
|
+
*/
|
|
203082
|
+
stopTips() {
|
|
203083
|
+
if (this.#tipIntervalId) {
|
|
203084
|
+
clearInterval(this.#tipIntervalId);
|
|
203085
|
+
this.#tipIntervalId = null;
|
|
203086
|
+
}
|
|
203087
|
+
if (this.#imageIntervalId) {
|
|
203088
|
+
clearInterval(this.#imageIntervalId);
|
|
203089
|
+
this.#imageIntervalId = null;
|
|
203090
|
+
}
|
|
203091
|
+
this.loadingAnimation.style.display = 'none';
|
|
203092
|
+
this.tipImageElement.style.display = 'none'; // Hide image
|
|
203093
|
+
this.tipImageElement.src = ''; // Clear image source
|
|
203094
|
+
this.tipImageContainer.style.display = 'none'; // Hide image container
|
|
203095
|
+
}
|
|
203096
|
+
|
|
203097
|
+
/**
|
|
203098
|
+
* Advances to the next tip and calls showCurrentTip() to display it.
|
|
203099
|
+
*/
|
|
203100
|
+
showNextTip() {
|
|
203101
|
+
this.#currentTipIndex = (this.#currentTipIndex + 1) % this.#tips.length;
|
|
203102
|
+
this.showCurrentTip();
|
|
203103
|
+
}
|
|
203104
|
+
|
|
203105
|
+
/**
|
|
203106
|
+
* Displays the current tip's text and manages its associated images.
|
|
203107
|
+
*/
|
|
203108
|
+
showCurrentTip() {
|
|
203109
|
+
const currentTip = this.#tips[this.#currentTipIndex];
|
|
203110
|
+
if (this.tipElement && currentTip) {
|
|
203111
|
+
this.tipElement.textContent = currentTip.text;
|
|
203112
|
+
|
|
203113
|
+
// Clear any existing image cycling interval for the previous tip
|
|
203114
|
+
if (this.#imageIntervalId) {
|
|
203115
|
+
clearInterval(this.#imageIntervalId);
|
|
203116
|
+
this.#imageIntervalId = null;
|
|
203117
|
+
}
|
|
203118
|
+
|
|
203119
|
+
// Image handling logic
|
|
203120
|
+
if (currentTip.images && currentTip.images.length > 0) {
|
|
203121
|
+
this.#currentImageIndex = 0; // Reset image index for the new tip
|
|
203122
|
+
this.showCurrentImage(currentTip.images[this.#currentImageIndex]);
|
|
203123
|
+
this.tipImageElement.style.display = 'block'; // Show the image
|
|
203124
|
+
this.tipImageContainer.style.display = 'flex'; // Show the image container
|
|
203125
|
+
|
|
203126
|
+
if (currentTip.images.length > 1) {
|
|
203127
|
+
// If there's more than one image, start cycling through them
|
|
203128
|
+
this.#imageIntervalId = setInterval(() => {
|
|
203129
|
+
this.#currentImageIndex = (this.#currentImageIndex + 1) % currentTip.images.length;
|
|
203130
|
+
this.showCurrentImage(currentTip.images[this.#currentImageIndex]);
|
|
203131
|
+
}, this.#imageDisplayDuration);
|
|
203132
|
+
}
|
|
203133
|
+
} else {
|
|
203134
|
+
// If no images are provided for this tip, hide the image elements
|
|
203135
|
+
this.tipImageElement.style.display = 'none';
|
|
203136
|
+
this.tipImageContainer.style.display = 'none';
|
|
203137
|
+
this.tipImageElement.src = ''; // Clear image source
|
|
203138
|
+
}
|
|
203139
|
+
}
|
|
203140
|
+
}
|
|
203141
|
+
|
|
203142
|
+
/**
|
|
203143
|
+
* Sets the source of the image element to display the given image.
|
|
203144
|
+
* @param {string} imageUrl - The URL of the image to display.
|
|
203145
|
+
*/
|
|
203146
|
+
showCurrentImage(imageUrl) {
|
|
203147
|
+
if (this.tipImageElement) {
|
|
203148
|
+
this.tipImageElement.src = imageUrl;
|
|
203149
|
+
}
|
|
203150
|
+
}
|
|
203151
|
+
}
|
|
203152
|
+
|
|
203153
|
+
// Define the custom element
|
|
203154
|
+
customElements.define("ide-loading-tips", IdeLoadingTips);
|
|
203155
|
+
|
|
203156
|
+
class IdeTipPopup extends HTMLElement {
|
|
203157
|
+
#ideLoadingTipsInstance = null; // aiLoadingTips 인스턴스를 저장할 변수
|
|
203158
|
+
#tipsUrl = '/data/tips.json'; // 팁 JSON 파일의 기본 경로 (public 폴더 내)
|
|
203159
|
+
#tipCategory = 'componentTips'; // 기본으로 표시할 팁 카테고리
|
|
203160
|
+
|
|
203161
|
+
constructor() {
|
|
203162
|
+
super();
|
|
203163
|
+
this.attachShadow({ mode: 'open' });
|
|
203164
|
+
}
|
|
203165
|
+
|
|
203166
|
+
connectedCallback() {
|
|
203167
|
+
this.shadowRoot.innerHTML = `
|
|
203168
|
+
<style>
|
|
203169
|
+
@import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/ideAssi.css";
|
|
203170
|
+
${ninegrid.getCustomPath(this,"ideAssi.css")}
|
|
203171
|
+
|
|
203172
|
+
/* ide-tip-popup 전용 스타일 (필요시 추가) */
|
|
203173
|
+
.dialog-header {
|
|
203174
|
+
font-size: 1.2em;
|
|
203175
|
+
font-weight: bold;
|
|
203176
|
+
margin-bottom: 15px;
|
|
203177
|
+
text-align: center;
|
|
203178
|
+
color: #333;
|
|
203179
|
+
padding-top: 10px; /* 헤더 위 공간 */
|
|
203180
|
+
}
|
|
203181
|
+
.dialog-content {
|
|
203182
|
+
/* aiLoadingTips가 들어갈 컨테이너 */
|
|
203183
|
+
padding: 0 5px;
|
|
203184
|
+
}
|
|
203185
|
+
/* nx-dialog의 닫기 버튼과 겹치지 않도록 조정 */
|
|
203186
|
+
nx-dialog::part(close-button) { /* nx-dialog의 내부 닫기 버튼에 접근 */
|
|
203187
|
+
top: 10px;
|
|
203188
|
+
right: 10px;
|
|
203189
|
+
}
|
|
203190
|
+
</style>
|
|
203191
|
+
|
|
203192
|
+
<nx-dialog>
|
|
203193
|
+
<div slot="header" class="dialog-header">AI가 답변을 준비 중입니다...</div>
|
|
203194
|
+
<div class="dialog-content"></div>
|
|
203195
|
+
<div slot="footer" class="dialog-footer"></div>
|
|
203196
|
+
</nx-dialog>
|
|
203197
|
+
`;
|
|
202920
203198
|
|
|
202921
203199
|
this.#init();
|
|
202922
203200
|
}
|
|
202923
203201
|
|
|
202924
203202
|
#init = () => {
|
|
202925
|
-
|
|
203203
|
+
// nx-dialog가 DOM에 추가된 후 내부 요소에 접근
|
|
203204
|
+
const dialog = this.shadowRoot.querySelector('nx-dialog');
|
|
203205
|
+
if (dialog) {
|
|
203206
|
+
// dialog가 닫힐 때 aiLoadingTips의 애니메이션 중지
|
|
203207
|
+
dialog.addEventListener('close', () => {
|
|
203208
|
+
if (this.#aiLoadingTipsInstance) {
|
|
203209
|
+
this.#aiLoadingTipsInstance.stopTips();
|
|
203210
|
+
// 팝업이 닫힐 때 aiLoadingTips 인스턴스 제거 (선택 사항)
|
|
203211
|
+
this.#aiLoadingTipsInstance.remove();
|
|
203212
|
+
this.#aiLoadingTipsInstance = null;
|
|
203213
|
+
}
|
|
203214
|
+
});
|
|
203215
|
+
}
|
|
202926
203216
|
}
|
|
202927
203217
|
|
|
202928
|
-
|
|
203218
|
+
/**
|
|
203219
|
+
* 팁 팝업을 표시하고 aiLoadingTips를 시작합니다.
|
|
203220
|
+
* @param {string} [category='componentTips'] - 표시할 팁의 카테고리 ('componentTips' 또는 'generalAITips')
|
|
203221
|
+
* @param {string} [tipsUrl='/data/tips.json'] - 팁 JSON 파일의 URL
|
|
203222
|
+
*/
|
|
203223
|
+
async popup(category = 'componentTips', tipsUrl = '/tips/tip.json') {
|
|
203224
|
+
this.#tipCategory = category;
|
|
203225
|
+
this.#tipsUrl = tipsUrl;
|
|
203226
|
+
|
|
203227
|
+
const dialogContent = this.shadowRoot.querySelector('.dialog-content');
|
|
203228
|
+
if (!dialogContent) {
|
|
203229
|
+
console.error("Popup content container not found.");
|
|
203230
|
+
return;
|
|
203231
|
+
}
|
|
203232
|
+
|
|
203233
|
+
// 기존 aiLoadingTips 인스턴스가 있다면 제거
|
|
203234
|
+
if (this.#ideLoadingTipsInstance) {
|
|
203235
|
+
this.#ideLoadingTipsInstance.stopTips();
|
|
203236
|
+
this.#ideLoadingTipsInstance.remove();
|
|
203237
|
+
this.#ideLoadingTipsInstance = null;
|
|
203238
|
+
}
|
|
203239
|
+
|
|
203240
|
+
// 새로운 aiLoadingTips 인스턴스 생성 및 추가
|
|
203241
|
+
this.#ideLoadingTipsInstance = document.createElement('ide-loading-tips');
|
|
203242
|
+
dialogContent.appendChild(this.#ideLoadingTipsInstance);
|
|
203243
|
+
|
|
203244
|
+
// 팁 로딩 시작
|
|
203245
|
+
await this.#ideLoadingTipsInstance.startTips(this.#tipsUrl, this.#tipCategory);
|
|
203246
|
+
|
|
203247
|
+
// nx-dialog 팝업 표시
|
|
202929
203248
|
this.shadowRoot.querySelector('nx-dialog')?.showModal();
|
|
202930
203249
|
};
|
|
202931
203250
|
|
|
203251
|
+
/**
|
|
203252
|
+
* 팁 팝업을 닫습니다.
|
|
203253
|
+
*/
|
|
202932
203254
|
close = () => {
|
|
202933
203255
|
this.shadowRoot.querySelector('nx-dialog')?.close();
|
|
203256
|
+
// close 이벤트 리스너에서 aiLoadingTips 정리 로직이 실행될 것임
|
|
202934
203257
|
};
|
|
202935
203258
|
}
|
|
202936
203259
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// aiLoadingTips.js
|
|
2
|
+
// This file contains the full source code for the nx-ai-loading-tips web component.
|
|
3
|
+
|
|
4
|
+
import ninegrid from "ninegrid2"; // Assuming ninegrid2 is available in your project
|
|
5
|
+
|
|
6
|
+
class IdeLoadingTips extends HTMLElement {
|
|
7
|
+
#tips = []; // Stores tip objects {text: string, images: string[]}
|
|
8
|
+
#currentTipIndex = 0; // Index of the currently displayed tip
|
|
9
|
+
#currentImageIndex = 0; // Index of the currently displayed image for the current tip
|
|
10
|
+
#tipIntervalId = null; // Interval ID for cycling through tips
|
|
11
|
+
#imageIntervalId = null; // Interval ID for cycling through images within a tip
|
|
12
|
+
#tipDisplayDuration = 5000; // How long each tip is displayed (5 seconds)
|
|
13
|
+
#imageDisplayDuration = 2000; // How long each image within a tip is displayed (2 seconds)
|
|
14
|
+
|
|
15
|
+
// DOM element references (cached for performance)
|
|
16
|
+
tipElement = null;
|
|
17
|
+
loadingAnimation = null;
|
|
18
|
+
tipImageElement = null;
|
|
19
|
+
tipImageContainer = null;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.attachShadow({ mode: 'open' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
// Set up the initial structure of the component in its Shadow DOM
|
|
28
|
+
this.shadowRoot.innerHTML = `
|
|
29
|
+
<style>
|
|
30
|
+
/* Import ninegrid's base AI CSS */
|
|
31
|
+
@import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/ai.css";
|
|
32
|
+
/* Import custom CSS specific to this component (adjust path as needed) */
|
|
33
|
+
${ninegrid.getCustomPath(this, "ai.css")}
|
|
34
|
+
|
|
35
|
+
/* Component-specific styles */
|
|
36
|
+
:host {
|
|
37
|
+
display: flex;
|
|
38
|
+
justify-content: flex-start;
|
|
39
|
+
padding: 5px;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.chat-message.loading-tips-message {
|
|
44
|
+
max-width: 80%;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
background-color: #fff;
|
|
48
|
+
color: black;
|
|
49
|
+
align-self: flex-start;
|
|
50
|
+
text-align: left;
|
|
51
|
+
position: relative;
|
|
52
|
+
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15);
|
|
53
|
+
padding: 12px 16px;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
align-items: flex-start;
|
|
57
|
+
padding-bottom: 8px; /* Adjusted for image container */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.loading-animation {
|
|
61
|
+
width: 20px;
|
|
62
|
+
height: 20px;
|
|
63
|
+
border: 3px solid #f3f3f3;
|
|
64
|
+
border-top: 3px solid #3498db;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
animation: spin 1s linear infinite;
|
|
67
|
+
margin-bottom: 8px;
|
|
68
|
+
display: none; /* Controlled by JS */
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.tip-content {
|
|
72
|
+
width: 100%;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.tip-label {
|
|
76
|
+
font-size: 0.85em;
|
|
77
|
+
color: #666;
|
|
78
|
+
margin: 0 0 5px 0;
|
|
79
|
+
font-weight: bold;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.current-tip {
|
|
83
|
+
font-size: 1em;
|
|
84
|
+
margin: 0;
|
|
85
|
+
color: #333;
|
|
86
|
+
line-height: 1.5;
|
|
87
|
+
margin-bottom: 10px; /* Space between text and image */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.tip-image-container {
|
|
91
|
+
width: 100%;
|
|
92
|
+
max-height: 150px; /* Max height for the image area */
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
display: flex;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
align-items: center;
|
|
97
|
+
background-color: #f0f0f0; /* Optional background for image area */
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
margin-top: 5px;
|
|
100
|
+
display: none; /* Controlled by JS */
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.tip-image {
|
|
104
|
+
max-width: 100%;
|
|
105
|
+
max-height: 100%;
|
|
106
|
+
object-fit: contain; /* Ensures image fits without cropping, maintaining aspect ratio */
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
display: none; /* Controlled by JS */
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@keyframes spin {
|
|
112
|
+
0% { transform: rotate(0deg); }
|
|
113
|
+
100% { transform: rotate(360deg); }
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
<div class="chat-message loading-tips-message">
|
|
117
|
+
<div class="loading-animation"></div>
|
|
118
|
+
<div class="tip-content">
|
|
119
|
+
<p class="tip-label">오늘의 AI 팁:</p>
|
|
120
|
+
<p class="current-tip"></p>
|
|
121
|
+
<div class="tip-image-container">
|
|
122
|
+
<img class="tip-image" src="" alt="팁 관련 이미지">
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
|
127
|
+
// Cache DOM element references
|
|
128
|
+
this.tipElement = this.shadowRoot.querySelector('.current-tip');
|
|
129
|
+
this.loadingAnimation = this.shadowRoot.querySelector('.loading-animation');
|
|
130
|
+
this.tipImageElement = this.shadowRoot.querySelector('.tip-image');
|
|
131
|
+
this.tipImageContainer = this.shadowRoot.querySelector('.tip-image-container');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
disconnectedCallback() {
|
|
135
|
+
// Clean up all intervals when the component is removed from the DOM
|
|
136
|
+
this.stopTips();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Loads tips asynchronously from a JSON URL and starts displaying them.
|
|
141
|
+
* @param {string} tipsUrl - The URL of the JSON file containing tip data.
|
|
142
|
+
* @param {string} tipCategory - The key in the JSON file's data that corresponds to the desired tip array (e.g., 'componentTips').
|
|
143
|
+
*/
|
|
144
|
+
async startTips(tipsUrl, tipCategory) {
|
|
145
|
+
if (!tipsUrl || !tipCategory) {
|
|
146
|
+
console.error("Tips URL or category not provided for aiLoadingTips.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(tipsUrl);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Failed to fetch tips: ${response.statusText}`);
|
|
154
|
+
}
|
|
155
|
+
const data = await response.json();
|
|
156
|
+
const tips = data[tipCategory];
|
|
157
|
+
|
|
158
|
+
if (!tips || tips.length === 0) {
|
|
159
|
+
console.warn(`No tips found for category '${tipCategory}' in ${tipsUrl}.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.#tips = tips;
|
|
164
|
+
this.#currentTipIndex = 0;
|
|
165
|
+
this.stopTips(); // Stop any existing intervals before starting new ones
|
|
166
|
+
|
|
167
|
+
this.showCurrentTip(); // Display the first tip immediately
|
|
168
|
+
|
|
169
|
+
// Start interval for cycling through tips
|
|
170
|
+
this.#tipIntervalId = setInterval(() => {
|
|
171
|
+
this.showNextTip();
|
|
172
|
+
}, this.#tipDisplayDuration);
|
|
173
|
+
|
|
174
|
+
this.loadingAnimation.style.display = 'block'; // Show the loading spinner
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error("Error loading tips:", error);
|
|
177
|
+
this.stopTips(); // Stop intervals and hide animations on error
|
|
178
|
+
if (this.tipElement) {
|
|
179
|
+
this.tipElement.textContent = "팁을 불러오는 중 오류가 발생했습니다.";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Stops all tip and image display intervals and hides related UI elements.
|
|
186
|
+
*/
|
|
187
|
+
stopTips() {
|
|
188
|
+
if (this.#tipIntervalId) {
|
|
189
|
+
clearInterval(this.#tipIntervalId);
|
|
190
|
+
this.#tipIntervalId = null;
|
|
191
|
+
}
|
|
192
|
+
if (this.#imageIntervalId) {
|
|
193
|
+
clearInterval(this.#imageIntervalId);
|
|
194
|
+
this.#imageIntervalId = null;
|
|
195
|
+
}
|
|
196
|
+
this.loadingAnimation.style.display = 'none';
|
|
197
|
+
this.tipImageElement.style.display = 'none'; // Hide image
|
|
198
|
+
this.tipImageElement.src = ''; // Clear image source
|
|
199
|
+
this.tipImageContainer.style.display = 'none'; // Hide image container
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Advances to the next tip and calls showCurrentTip() to display it.
|
|
204
|
+
*/
|
|
205
|
+
showNextTip() {
|
|
206
|
+
this.#currentTipIndex = (this.#currentTipIndex + 1) % this.#tips.length;
|
|
207
|
+
this.showCurrentTip();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Displays the current tip's text and manages its associated images.
|
|
212
|
+
*/
|
|
213
|
+
showCurrentTip() {
|
|
214
|
+
const currentTip = this.#tips[this.#currentTipIndex];
|
|
215
|
+
if (this.tipElement && currentTip) {
|
|
216
|
+
this.tipElement.textContent = currentTip.text;
|
|
217
|
+
|
|
218
|
+
// Clear any existing image cycling interval for the previous tip
|
|
219
|
+
if (this.#imageIntervalId) {
|
|
220
|
+
clearInterval(this.#imageIntervalId);
|
|
221
|
+
this.#imageIntervalId = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Image handling logic
|
|
225
|
+
if (currentTip.images && currentTip.images.length > 0) {
|
|
226
|
+
this.#currentImageIndex = 0; // Reset image index for the new tip
|
|
227
|
+
this.showCurrentImage(currentTip.images[this.#currentImageIndex]);
|
|
228
|
+
this.tipImageElement.style.display = 'block'; // Show the image
|
|
229
|
+
this.tipImageContainer.style.display = 'flex'; // Show the image container
|
|
230
|
+
|
|
231
|
+
if (currentTip.images.length > 1) {
|
|
232
|
+
// If there's more than one image, start cycling through them
|
|
233
|
+
this.#imageIntervalId = setInterval(() => {
|
|
234
|
+
this.#currentImageIndex = (this.#currentImageIndex + 1) % currentTip.images.length;
|
|
235
|
+
this.showCurrentImage(currentTip.images[this.#currentImageIndex]);
|
|
236
|
+
}, this.#imageDisplayDuration);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// If no images are provided for this tip, hide the image elements
|
|
240
|
+
this.tipImageElement.style.display = 'none';
|
|
241
|
+
this.tipImageContainer.style.display = 'none';
|
|
242
|
+
this.tipImageElement.src = ''; // Clear image source
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Sets the source of the image element to display the given image.
|
|
249
|
+
* @param {string} imageUrl - The URL of the image to display.
|
|
250
|
+
*/
|
|
251
|
+
showCurrentImage(imageUrl) {
|
|
252
|
+
if (this.tipImageElement) {
|
|
253
|
+
this.tipImageElement.src = imageUrl;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Define the custom element
|
|
259
|
+
customElements.define("ide-loading-tips", IdeLoadingTips);
|