@hirohsu/user-web-feedback 2.6.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/LICENSE +21 -0
- package/README.md +953 -0
- package/dist/cli.cjs +95778 -0
- package/dist/index.cjs +92818 -0
- package/dist/static/app.js +385 -0
- package/dist/static/components/navbar.css +406 -0
- package/dist/static/components/navbar.html +49 -0
- package/dist/static/components/navbar.js +211 -0
- package/dist/static/dashboard.css +495 -0
- package/dist/static/dashboard.html +95 -0
- package/dist/static/dashboard.js +540 -0
- package/dist/static/favicon.svg +27 -0
- package/dist/static/index.html +541 -0
- package/dist/static/logs.html +376 -0
- package/dist/static/logs.js +442 -0
- package/dist/static/mcp-settings.html +797 -0
- package/dist/static/mcp-settings.js +884 -0
- package/dist/static/modules/app-core.js +124 -0
- package/dist/static/modules/conversation-panel.js +247 -0
- package/dist/static/modules/feedback-handler.js +1420 -0
- package/dist/static/modules/image-handler.js +155 -0
- package/dist/static/modules/log-viewer.js +296 -0
- package/dist/static/modules/mcp-manager.js +474 -0
- package/dist/static/modules/prompt-manager.js +364 -0
- package/dist/static/modules/settings-manager.js +299 -0
- package/dist/static/modules/socket-manager.js +170 -0
- package/dist/static/modules/state-manager.js +352 -0
- package/dist/static/modules/timer-controller.js +243 -0
- package/dist/static/modules/ui-helpers.js +246 -0
- package/dist/static/settings.html +355 -0
- package/dist/static/settings.js +425 -0
- package/dist/static/socket.io.min.js +7 -0
- package/dist/static/style.css +2157 -0
- package/dist/static/terminals.html +357 -0
- package/dist/static/terminals.js +321 -0
- package/package.json +91 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timer-controller.js
|
|
3
|
+
* 計時器管理模組
|
|
4
|
+
* 包含對話超時、關閉倒數、自動回覆等計時器功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getDialogTimeoutSeconds,
|
|
9
|
+
getAutoReplyTimerSeconds,
|
|
10
|
+
getAutoReplyTimerRemaining,
|
|
11
|
+
setAutoReplyTimerRemaining,
|
|
12
|
+
isAutoReplyTimerPaused,
|
|
13
|
+
setAutoReplyTimerPaused,
|
|
14
|
+
isAutoReplyPausedByFocus,
|
|
15
|
+
setAutoReplyPausedByFocus,
|
|
16
|
+
getDialogTimeoutInterval,
|
|
17
|
+
setDialogTimeoutInterval,
|
|
18
|
+
getCloseCountdownInterval,
|
|
19
|
+
setCloseCountdownInterval,
|
|
20
|
+
getAutoReplyTimerInterval,
|
|
21
|
+
setAutoReplyTimerInterval,
|
|
22
|
+
getAutoReplyCountdownInterval,
|
|
23
|
+
setAutoReplyCountdownInterval,
|
|
24
|
+
getAutoReplyWarningTimeout,
|
|
25
|
+
setAutoReplyWarningTimeout,
|
|
26
|
+
getAutoReplyConfirmationTimeout,
|
|
27
|
+
setAutoReplyConfirmationTimeout,
|
|
28
|
+
} from "./state-manager.js";
|
|
29
|
+
|
|
30
|
+
// 使用動態導入打破循環依賴
|
|
31
|
+
let triggerAutoAIReplyFn = null;
|
|
32
|
+
async function loadTriggerAutoAIReply() {
|
|
33
|
+
if (!triggerAutoAIReplyFn) {
|
|
34
|
+
const module = await import("./feedback-handler.js");
|
|
35
|
+
triggerAutoAIReplyFn = module.triggerAutoAIReply;
|
|
36
|
+
}
|
|
37
|
+
return triggerAutoAIReplyFn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 停止所有計時器
|
|
42
|
+
*/
|
|
43
|
+
export function stopAllTimers() {
|
|
44
|
+
const dialogTimeout = getDialogTimeoutInterval();
|
|
45
|
+
if (dialogTimeout) {
|
|
46
|
+
clearInterval(dialogTimeout);
|
|
47
|
+
setDialogTimeoutInterval(null);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const closeCountdown = getCloseCountdownInterval();
|
|
51
|
+
if (closeCountdown) {
|
|
52
|
+
clearInterval(closeCountdown);
|
|
53
|
+
setCloseCountdownInterval(null);
|
|
54
|
+
}
|
|
55
|
+
const countdownEl = document.getElementById("close-cd");
|
|
56
|
+
if (countdownEl) {
|
|
57
|
+
countdownEl.style.display = "none";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const autoReplyTimer = getAutoReplyTimerInterval();
|
|
61
|
+
if (autoReplyTimer) {
|
|
62
|
+
clearInterval(autoReplyTimer);
|
|
63
|
+
setAutoReplyTimerInterval(null);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 隱藏自動回覆計時器
|
|
67
|
+
const autoReplyTimerEl = document.getElementById("auto-reply-timer");
|
|
68
|
+
if (autoReplyTimerEl) {
|
|
69
|
+
autoReplyTimerEl.classList.remove("active", "paused");
|
|
70
|
+
autoReplyTimerEl.style.display = "none";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const confirmationTimeout = getAutoReplyConfirmationTimeout();
|
|
74
|
+
if (confirmationTimeout) {
|
|
75
|
+
clearInterval(confirmationTimeout);
|
|
76
|
+
setAutoReplyConfirmationTimeout(null);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 開始關閉頁面倒數計時(提交成功後 3 秒關閉)
|
|
82
|
+
*/
|
|
83
|
+
export function startCloseCountdown() {
|
|
84
|
+
const countdownEl = document.getElementById("close-cd");
|
|
85
|
+
if (!countdownEl) {
|
|
86
|
+
console.warn("關閉倒數計時器元素 (close-cd) 未找到");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const existingInterval = getCloseCountdownInterval();
|
|
91
|
+
if (existingInterval) {
|
|
92
|
+
clearInterval(existingInterval);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 固定 3 秒倒數
|
|
96
|
+
let remaining = 3;
|
|
97
|
+
countdownEl.style.display = "inline-flex";
|
|
98
|
+
countdownEl.textContent = remaining;
|
|
99
|
+
|
|
100
|
+
console.log(`開始關閉頁面倒數計時: ${remaining} 秒`);
|
|
101
|
+
|
|
102
|
+
const intervalId = setInterval(() => {
|
|
103
|
+
remaining--;
|
|
104
|
+
countdownEl.textContent = remaining;
|
|
105
|
+
|
|
106
|
+
if (remaining <= 0) {
|
|
107
|
+
clearInterval(intervalId);
|
|
108
|
+
setCloseCountdownInterval(null);
|
|
109
|
+
console.log("倒數結束,關閉頁面");
|
|
110
|
+
window.close();
|
|
111
|
+
}
|
|
112
|
+
}, 1000);
|
|
113
|
+
|
|
114
|
+
setCloseCountdownInterval(intervalId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 取得計時器元素(使用連接狀態區塊內的計時器)
|
|
119
|
+
*/
|
|
120
|
+
function getTimerElements() {
|
|
121
|
+
// 使用連接狀態區塊內的計時器
|
|
122
|
+
let timerEl = document.getElementById("auto-reply-timer");
|
|
123
|
+
let secondsEl = document.getElementById("auto-reply-seconds");
|
|
124
|
+
|
|
125
|
+
return { timerEl, secondsEl };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 開始自動回應倒數計時(300 秒)
|
|
130
|
+
*/
|
|
131
|
+
export function startAutoReplyTimer() {
|
|
132
|
+
const { timerEl, secondsEl } = getTimerElements();
|
|
133
|
+
|
|
134
|
+
if (!timerEl || !secondsEl) {
|
|
135
|
+
console.warn("自動回應計時器元素未找到");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setAutoReplyTimerRemaining(getAutoReplyTimerSeconds());
|
|
140
|
+
timerEl.style.display = "inline-flex";
|
|
141
|
+
timerEl.classList.add("active");
|
|
142
|
+
timerEl.classList.remove("paused");
|
|
143
|
+
|
|
144
|
+
const existingInterval = getAutoReplyTimerInterval();
|
|
145
|
+
if (existingInterval) {
|
|
146
|
+
clearInterval(existingInterval);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const updateCountdown = async () => {
|
|
150
|
+
const { secondsEl: currentSecondsEl } = getTimerElements();
|
|
151
|
+
if (!currentSecondsEl) return;
|
|
152
|
+
|
|
153
|
+
if (isAutoReplyTimerPaused()) {
|
|
154
|
+
currentSecondsEl.textContent = getAutoReplyTimerRemaining();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const remaining = getAutoReplyTimerRemaining();
|
|
159
|
+
if (remaining > 0) {
|
|
160
|
+
currentSecondsEl.textContent = remaining;
|
|
161
|
+
setAutoReplyTimerRemaining(remaining - 1);
|
|
162
|
+
} else {
|
|
163
|
+
const interval = getAutoReplyTimerInterval();
|
|
164
|
+
if (interval) {
|
|
165
|
+
clearInterval(interval);
|
|
166
|
+
setAutoReplyTimerInterval(null);
|
|
167
|
+
}
|
|
168
|
+
console.log("自動回應時間已到,啟動 AI 回應");
|
|
169
|
+
// 動態載入並執行
|
|
170
|
+
const triggerFn = await loadTriggerAutoAIReply();
|
|
171
|
+
if (triggerFn) triggerFn();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
secondsEl.textContent = getAutoReplyTimerRemaining();
|
|
176
|
+
if (!isAutoReplyTimerPaused()) {
|
|
177
|
+
setAutoReplyTimerRemaining(getAutoReplyTimerRemaining() - 1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const intervalId = setInterval(updateCountdown, 1000);
|
|
181
|
+
setAutoReplyTimerInterval(intervalId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 暫停自動回覆計時器
|
|
186
|
+
* @param {boolean} byFocus - 是否由焦點事件引起的暫停
|
|
187
|
+
*/
|
|
188
|
+
export function pauseAutoReplyTimer(byFocus = false) {
|
|
189
|
+
setAutoReplyTimerPaused(true);
|
|
190
|
+
if (byFocus) setAutoReplyPausedByFocus(true);
|
|
191
|
+
|
|
192
|
+
const { timerEl } = getTimerElements();
|
|
193
|
+
if (timerEl) timerEl.classList.add("paused");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 恢復自動回覆計時器
|
|
198
|
+
*/
|
|
199
|
+
export function resumeAutoReplyTimer() {
|
|
200
|
+
setAutoReplyTimerPaused(false);
|
|
201
|
+
setAutoReplyPausedByFocus(false);
|
|
202
|
+
|
|
203
|
+
const { timerEl } = getTimerElements();
|
|
204
|
+
if (timerEl) timerEl.classList.remove("paused");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 停止自動回覆倒數計時
|
|
209
|
+
*/
|
|
210
|
+
export function stopAutoReplyCountdown() {
|
|
211
|
+
const interval = getAutoReplyCountdownInterval();
|
|
212
|
+
if (interval) {
|
|
213
|
+
clearInterval(interval);
|
|
214
|
+
setAutoReplyCountdownInterval(null);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 顯示自動回覆警告(已棄用,改用底部計時器)
|
|
220
|
+
* @deprecated
|
|
221
|
+
*/
|
|
222
|
+
export function showAutoReplyWarning(seconds) {
|
|
223
|
+
// 不再使用警告彈窗,計時器會常駐在底部
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 隱藏自動回覆警告(已棄用)
|
|
228
|
+
* @deprecated
|
|
229
|
+
*/
|
|
230
|
+
export function hideAutoReplyWarning() {
|
|
231
|
+
// 不再使用警告彈窗
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default {
|
|
235
|
+
stopAllTimers,
|
|
236
|
+
startCloseCountdown,
|
|
237
|
+
startAutoReplyTimer,
|
|
238
|
+
pauseAutoReplyTimer,
|
|
239
|
+
resumeAutoReplyTimer,
|
|
240
|
+
stopAutoReplyCountdown,
|
|
241
|
+
showAutoReplyWarning,
|
|
242
|
+
hideAutoReplyWarning,
|
|
243
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-helpers.js
|
|
3
|
+
* UI 輔助函數模組
|
|
4
|
+
* 包含 Toast、Modal、Loading、HTML 轉義等工具函數
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HTML 轉義
|
|
9
|
+
* @param {string} text - 要轉義的文字
|
|
10
|
+
* @returns {string} - 轉義後的文字
|
|
11
|
+
*/
|
|
12
|
+
export function escapeHtml(text) {
|
|
13
|
+
if (!text) return "";
|
|
14
|
+
const div = document.createElement("div");
|
|
15
|
+
div.textContent = text;
|
|
16
|
+
return div.innerHTML;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 正則表達式轉義
|
|
21
|
+
* @param {string} str - 要轉義的字串
|
|
22
|
+
* @returns {string} - 轉義後的字串
|
|
23
|
+
*/
|
|
24
|
+
export function escapeRegex(str) {
|
|
25
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 顯示 Toast 通知
|
|
30
|
+
* @param {string} type - 類型: success, error, info
|
|
31
|
+
* @param {string} title - 標題
|
|
32
|
+
* @param {string} message - 訊息
|
|
33
|
+
*/
|
|
34
|
+
export function showToast(type, title, message) {
|
|
35
|
+
const container = document.getElementById("toastContainer");
|
|
36
|
+
if (!container) return;
|
|
37
|
+
|
|
38
|
+
const toast = document.createElement("div");
|
|
39
|
+
toast.className = `toast ${type}`;
|
|
40
|
+
toast.innerHTML = `
|
|
41
|
+
<div class="toast-icon">${getToastIcon(type)}</div>
|
|
42
|
+
<div class="toast-content">
|
|
43
|
+
<div class="toast-title">${escapeHtml(title)}</div>
|
|
44
|
+
<div class="toast-message">${escapeHtml(message)}</div>
|
|
45
|
+
</div>
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
container.appendChild(toast);
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
toast.style.opacity = "0";
|
|
52
|
+
setTimeout(() => toast.remove(), 300);
|
|
53
|
+
}, 3000);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 取得 Toast 圖標
|
|
58
|
+
* @param {string} type - 類型
|
|
59
|
+
* @returns {string} - 圖標
|
|
60
|
+
*/
|
|
61
|
+
function getToastIcon(type) {
|
|
62
|
+
const icons = {
|
|
63
|
+
success: "✅",
|
|
64
|
+
error: "❌",
|
|
65
|
+
info: "ℹ️",
|
|
66
|
+
};
|
|
67
|
+
return icons[type] || "📢";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 格式化 API 錯誤為字串
|
|
72
|
+
* @param {Object|string} data - 錯誤資料
|
|
73
|
+
* @returns {string} - 格式化後的錯誤訊息
|
|
74
|
+
*/
|
|
75
|
+
export function formatApiError(data) {
|
|
76
|
+
if (!data) return "未知錯誤";
|
|
77
|
+
if (typeof data === "string") return data;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (data.error) parts.push(data.error);
|
|
82
|
+
if (data.details) {
|
|
83
|
+
parts.push(
|
|
84
|
+
typeof data.details === "string"
|
|
85
|
+
? data.details
|
|
86
|
+
: JSON.stringify(data.details)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (data.stack) parts.push(data.stack);
|
|
90
|
+
return parts.join("\n") || JSON.stringify(data);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return String(data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 顯示提醒彈窗
|
|
98
|
+
* @param {string} title - 標題
|
|
99
|
+
* @param {string} message - 訊息
|
|
100
|
+
* @param {Function} onConfirm - 確認回調
|
|
101
|
+
* @param {Function} onCancel - 取消回調
|
|
102
|
+
*/
|
|
103
|
+
export function showAlertModal(
|
|
104
|
+
title,
|
|
105
|
+
message,
|
|
106
|
+
onConfirm = null,
|
|
107
|
+
onCancel = null
|
|
108
|
+
) {
|
|
109
|
+
const modal = document.getElementById("alertModal");
|
|
110
|
+
if (!modal) return;
|
|
111
|
+
|
|
112
|
+
const titleEl = document.getElementById("alertModalTitle");
|
|
113
|
+
const bodyEl = document.getElementById("alertModalBody");
|
|
114
|
+
const confirmBtn = document.getElementById("alertModalConfirm");
|
|
115
|
+
const cancelBtn = document.getElementById("alertModalCancel");
|
|
116
|
+
|
|
117
|
+
if (titleEl) titleEl.textContent = title;
|
|
118
|
+
if (bodyEl) bodyEl.textContent = message;
|
|
119
|
+
|
|
120
|
+
// 設置確認按鈕
|
|
121
|
+
if (confirmBtn) {
|
|
122
|
+
confirmBtn.onclick = () => {
|
|
123
|
+
hideAlertModal();
|
|
124
|
+
if (onConfirm) onConfirm();
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 設置取消按鈕
|
|
129
|
+
if (cancelBtn) {
|
|
130
|
+
if (onCancel) {
|
|
131
|
+
cancelBtn.style.display = "block";
|
|
132
|
+
cancelBtn.onclick = () => {
|
|
133
|
+
hideAlertModal();
|
|
134
|
+
onCancel();
|
|
135
|
+
};
|
|
136
|
+
} else {
|
|
137
|
+
cancelBtn.style.display = "none";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
modal.classList.add("show");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 隱藏提醒彈窗
|
|
146
|
+
*/
|
|
147
|
+
export function hideAlertModal() {
|
|
148
|
+
const modal = document.getElementById("alertModal");
|
|
149
|
+
if (modal) {
|
|
150
|
+
modal.classList.remove("show");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 顯示載入遮罩
|
|
156
|
+
* @param {string} text - 載入文字
|
|
157
|
+
*/
|
|
158
|
+
export function showLoadingOverlay(text = "處理中...") {
|
|
159
|
+
const overlay = document.getElementById("loadingOverlay");
|
|
160
|
+
const loadingText = document.getElementById("loadingText");
|
|
161
|
+
|
|
162
|
+
if (loadingText) loadingText.textContent = text;
|
|
163
|
+
if (overlay) overlay.style.display = "flex";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 隱藏載入遮罩
|
|
168
|
+
*/
|
|
169
|
+
export function hideLoadingOverlay() {
|
|
170
|
+
const overlay = document.getElementById("loadingOverlay");
|
|
171
|
+
if (overlay) overlay.style.display = "none";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 顯示專案資訊
|
|
176
|
+
* @param {string} projectName - 專案名稱
|
|
177
|
+
* @param {string} projectPath - 專案路徑
|
|
178
|
+
*/
|
|
179
|
+
export function displayProjectInfo(projectName, projectPath) {
|
|
180
|
+
const projectInfoEl = document.getElementById("projectInfo");
|
|
181
|
+
if (!projectInfoEl) return;
|
|
182
|
+
|
|
183
|
+
if (projectName || projectPath) {
|
|
184
|
+
const name = projectName || "未命名專案";
|
|
185
|
+
const path = projectPath ? ` (${projectPath})` : "";
|
|
186
|
+
projectInfoEl.innerHTML = `<span class="icon">📁</span> ${name}${path}`;
|
|
187
|
+
projectInfoEl.title = projectPath || projectName || "";
|
|
188
|
+
} else {
|
|
189
|
+
projectInfoEl.innerHTML = "";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 顯示 AI 訊息
|
|
195
|
+
* @param {string} message - 訊息內容 (Markdown)
|
|
196
|
+
*/
|
|
197
|
+
export function displayAIMessage(message) {
|
|
198
|
+
const displayEl = document.getElementById("aiMessageDisplay");
|
|
199
|
+
if (!displayEl) return;
|
|
200
|
+
|
|
201
|
+
const htmlContent = marked.parse(message);
|
|
202
|
+
displayEl.innerHTML = `<div class="ai-message-content">${htmlContent}</div>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 更新字元計數
|
|
207
|
+
*/
|
|
208
|
+
export function updateCharCount() {
|
|
209
|
+
const textEl = document.getElementById("feedbackText");
|
|
210
|
+
const countEl = document.getElementById("charCount");
|
|
211
|
+
|
|
212
|
+
if (textEl && countEl) {
|
|
213
|
+
countEl.textContent = `${textEl.value.length} 字元`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 截斷過長的文字
|
|
219
|
+
* @param {string|Object} text - 要截斷的文字
|
|
220
|
+
* @param {number} maxLength - 最大長度
|
|
221
|
+
* @returns {string} - 截斷後的文字
|
|
222
|
+
*/
|
|
223
|
+
export function truncateResult(text, maxLength = 500) {
|
|
224
|
+
if (typeof text !== "string") {
|
|
225
|
+
text = JSON.stringify(text, null, 2);
|
|
226
|
+
}
|
|
227
|
+
if (text.length > maxLength) {
|
|
228
|
+
return text.substring(0, maxLength) + "\n... (已截斷)";
|
|
229
|
+
}
|
|
230
|
+
return text;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default {
|
|
234
|
+
escapeHtml,
|
|
235
|
+
escapeRegex,
|
|
236
|
+
showToast,
|
|
237
|
+
formatApiError,
|
|
238
|
+
showAlertModal,
|
|
239
|
+
hideAlertModal,
|
|
240
|
+
showLoadingOverlay,
|
|
241
|
+
hideLoadingOverlay,
|
|
242
|
+
displayProjectInfo,
|
|
243
|
+
displayAIMessage,
|
|
244
|
+
updateCharCount,
|
|
245
|
+
truncateResult,
|
|
246
|
+
};
|