diff --git a/src/lib/common/InPlaceEdit.svelte b/src/lib/common/InPlaceEdit.svelte index 527c0579..9ce32362 100644 --- a/src/lib/common/InPlaceEdit.svelte +++ b/src/lib/common/InPlaceEdit.svelte @@ -31,7 +31,12 @@ dispatch('submit', value); } } - + + /** @param {any} event */ + function handleInput(event) { + dispatch('input', event); + } + /** @param {any} event */ function keydown(event) { if (event.key == 'Escape') { @@ -57,6 +62,7 @@ maxlength={maxLength} use:focus on:blur={() => submit()} + on:input={handleInput} /> {:else} diff --git a/src/lib/common/RemoteSearchInput.svelte b/src/lib/common/RemoteSearchInput.svelte new file mode 100644 index 00000000..b69f4635 --- /dev/null +++ b/src/lib/common/RemoteSearchInput.svelte @@ -0,0 +1,84 @@ + + +
+ 0 || loading)} toggle={() => isOpen = !isOpen}> + + + + + {#if loading} + + + + {:else} + {#each searchResults as result, index} + selectResult(result)} + title={result.name} + > + {result.name} + + {/each} + {/if} + + +
\ No newline at end of file diff --git a/src/lib/common/nav-bar/NavItem.svelte b/src/lib/common/nav-bar/NavItem.svelte index 8ebed829..0c6ffd42 100644 --- a/src/lib/common/nav-bar/NavItem.svelte +++ b/src/lib/common/nav-bar/NavItem.svelte @@ -68,6 +68,9 @@ /** @type {() => void} */ export let onDelete = () => {}; + /** @type {() => void} */ + export let onInput = () => {}; + /** @param {any} e */ function handleTabClick(e) { e.preventDefault(); @@ -79,6 +82,12 @@ e.preventDefault(); onDelete?.(); } + + /** @param {any} e */ + function handleTabInput(e) { + e.preventDefault(); + onInput?.(); + }
  • handleTabClick(e)} > {#if allowEdit} - + {:else}
    {navBtnText}
    diff --git a/src/lib/helpers/http.js b/src/lib/helpers/http.js index 06f52516..4a113b32 100644 --- a/src/lib/helpers/http.js +++ b/src/lib/helpers/http.js @@ -90,6 +90,7 @@ function skipLoader(config) { new RegExp('http(s*)://(.*?)/role/(.*?)/details', 'g'), new RegExp('http(s*)://(.*?)/user/(.*?)/details', 'g'), new RegExp('http(s*)://(.*?)/agent/labels', 'g'), + new RegExp('http(s*)://(.*?)/conversation/state-search', 'g'), ]; if (config.method === 'post' && postRegexes.some(regex => regex.test(config.url || ''))) { diff --git a/src/lib/helpers/utils/storage-manager.js b/src/lib/helpers/utils/storage-manager.js new file mode 100644 index 00000000..0a15d527 --- /dev/null +++ b/src/lib/helpers/utils/storage-manager.js @@ -0,0 +1,155 @@ +class LocalStorageManager { + /** + * @param {{ maxSize?: number, overflowStrategy?: 'LRU' | 'EXPIRE_FIRST' }} options + */ + constructor(options = {}) { + this.maxSize = options.maxSize || 4 * 1024 * 1024; + this.overflowStrategy = options.overflowStrategy || 'EXPIRE_FIRST'; + } + + /** + * @param {string} key + * @param {any} value + * @param {number | null} ttl + */ + set(key, value, ttl = null) { + try { + const item = { + value, + meta: { + expire: ttl ? Date.now() + ttl : null, + lastAccess: Date.now() + } + }; + + const cost = this._calculateItemCost(key, JSON.stringify(item)); + + if (cost > this.maxSize) throw new Error('Item exceeds maximum storage size'); + + if (this._getTotalSize() + cost > this.maxSize) { + this._performCleanup(cost); + } + + if (this._getTotalSize() + cost > this.maxSize) throw new Error('Item exceeds maximum storage size'); + + localStorage.setItem(key, JSON.stringify(item)); + this._updateSizeCache(cost); + } catch (/** @type {any} */ error) { + console.error('Storage Error:', error); + } + } + + /** + * @param {string} key + * @returns {any} + */ + get(key) { + const raw = localStorage.getItem(key); + if (!raw) return null; + + const item = JSON.parse(raw); + if (this._isExpired(item)) { + this.remove(key); + return null; + } + + item.meta.lastAccess = Date.now(); + localStorage.setItem(key, JSON.stringify(item)); + return item.value; + } + + /** + * @param {string} key + */ + remove(key) { + const raw = localStorage.getItem(key); + localStorage.removeItem(key); + if (raw) this._updateSizeCache(-this._calculateItemCost(key, raw)); + } + + clear() { + localStorage.clear(); + this._sizeCache = 0; + } + + /** + * @param {any} item + * @returns {boolean} + */ + _isExpired(item) { + return item && item.meta && item.meta.expire && item.meta.expire < Date.now(); + } + + /** + * @param {string} key + * @param {string} valueString + * @returns {number} + */ + _calculateItemCost(key, valueString) { + const encoder = new TextEncoder(); + return encoder.encode(key).length + encoder.encode(valueString).length; + } + + _getTotalSize() { + if (!this._sizeCache) this._rebuildSizeCache(); + return this._sizeCache; + } + + _rebuildSizeCache() { + this._sizeCache = Array.from({ length: localStorage.length }) + .reduce((total, _, i) => { + const key = localStorage.key(i); + const item = key ? localStorage.getItem(key) : null; + return total + (key && item ? this._calculateItemCost(key, item) : 0); + }, 0); + } + + /** + * @param {number} delta + */ + _updateSizeCache(delta) { + this._sizeCache = (this._sizeCache || 0) + delta; + } + + /** + * @param {number} requiredSpace + */ + _performCleanup(requiredSpace) { + const /** @type {any[]} */ candidates = []; + + Array.from({ length: localStorage.length }).forEach((_, i) => { + const key = localStorage.key(i); + const raw = key ? localStorage.getItem(key) : null; + if (!key || !raw) { + return; + } + const item = JSON.parse(raw); + if (item && item.meta) { + candidates.push({ + key, + size: this._calculateItemCost(key, raw), + expire: item.meta.expire || Infinity, + lastAccess: item.meta.lastAccess + }); + } + }); + + switch (this.overflowStrategy) { + case 'EXPIRE_FIRST': + candidates.sort((a, b) => a.expire - b.expire); + break; + case 'LRU': + candidates.sort((a, b) => a.lastAccess - b.lastAccess); + break; + } + + let freedSpace = 0; + while (freedSpace < requiredSpace && candidates.length > 0) { + const target = candidates.shift(); + this.remove(target.key); + freedSpace += target.size; + } + } +} + +export default LocalStorageManager; \ No newline at end of file diff --git a/src/lib/langs/en.json b/src/lib/langs/en.json index cb0dd174..7f905c0b 100644 --- a/src/lib/langs/en.json +++ b/src/lib/langs/en.json @@ -300,5 +300,6 @@ "Account Origin":"Account Origin", "Update Date":"Update Date", "Create Date":"Create Date", - "Active now":"Active now" + "Active now":"Active now", + "Reset":"Reset" } \ No newline at end of file diff --git a/src/lib/langs/zh.json b/src/lib/langs/zh.json index d5c85db6..c2c06b19 100644 --- a/src/lib/langs/zh.json +++ b/src/lib/langs/zh.json @@ -495,5 +495,6 @@ "Account Origin":"账户起源", "Update Date":"更新日期", "Create Date":"创建日期", - "Active now":"正在工作" + "Active now":"正在工作", + "Reset":"重置" } \ No newline at end of file diff --git a/src/lib/scss/custom/common/_common.scss b/src/lib/scss/custom/common/_common.scss index 211d44ca..69efba04 100644 --- a/src/lib/scss/custom/common/_common.scss +++ b/src/lib/scss/custom/common/_common.scss @@ -180,3 +180,15 @@ button:focus { .thin-scrollbar { scrollbar-width: thin; } + +.scrollable-dropdown { + .dropdown-menu { + max-height: 200px; + overflow-y: auto; + } + .dropdown-item { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/lib/services/api-endpoints.js b/src/lib/services/api-endpoints.js index df5758c8..b42c4cb3 100644 --- a/src/lib/services/api-endpoints.js +++ b/src/lib/services/api-endpoints.js @@ -67,6 +67,8 @@ export const endpoints = { conversationTagsUpdateUrl: `${host}/conversation/{conversationId}/update-tags`, fileUploadUrl: `${host}/agent/{agentId}/conversation/{conversationId}/upload`, pinConversationUrl: `${host}/agent/{agentId}/conversation/{conversationId}/dashboard`, + conversationStateValueUrl: `${host}/conversation/state-search`, + conversationStateKeyListUrl: `${host}/conversation/state-key`, // LLM provider llmProvidersUrl: `${host}/llm-providers`, diff --git a/src/lib/services/conversation-service.js b/src/lib/services/conversation-service.js index 91d8a261..16124019 100644 --- a/src/lib/services/conversation-service.js +++ b/src/lib/services/conversation-service.js @@ -288,4 +288,38 @@ export async function getAddressOptions(text) { } }); return response.data; +} + +/** + * get conversation state key list + * @returns {Promise<{id: string, name: string, description: string}[]>} + */ +export async function getConversationStateKey() { + let url = endpoints.conversationStateKeyListUrl; + const response = await axios.get(url); + return response.data; +} + +/** @type {import('axios').CancelTokenSource | null} */ +let getConversationStateValueCancelToken = null; +/** + * get conversation state value + * @param {string} key + * @param {string} query + * @returns {Promise<{id: string, name: string}[]>} + */ +export async function getConversationStateValue(key, query) { + let url = endpoints.conversationStateValueUrl; + if (getConversationStateValueCancelToken) { + getConversationStateValueCancelToken.cancel(); + } + getConversationStateValueCancelToken = axios.CancelToken.source(); + const response = await axios.get(url, { + params: { + conversatinFilterType: key, + searchKey: query + }, + cancelToken: getConversationStateValueCancelToken.token + }); + return response.data; } \ No newline at end of file diff --git a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index 75f11709..4cd7788b 100644 --- a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -70,6 +70,7 @@ import ChatBigMessage from './chat-util/chat-big-message.svelte'; import PersistLog from './persist-log/persist-log.svelte'; import InstantLog from './instant-log/instant-log.svelte'; + import LocalStorageManager from '$lib/helpers/utils/storage-manager'; const options = { @@ -97,6 +98,8 @@ /** @type {import('$userTypes').UserModel} */ export let currentUser; + const messageStorage = new LocalStorageManager(); + /** @type {string} */ let text = ''; let editText = ''; @@ -220,6 +223,10 @@ selectedTags = conversation?.tags || []; initUserSentMessages(dialogs); initChatView(); + const messageDraft = getMessageDraft(); + if (messageDraft) { + text = messageDraft; + } handlePaneResize(); signalr.onMessageReceivedFromClient = onMessageReceivedFromClient; @@ -613,6 +620,7 @@ sendMessageToHub(agentId, convId, msgText, messageData).then(res => { resolve(res); + deleteMessageDraft(); }).catch(err => { reject(err); }).finally(() => { @@ -638,6 +646,7 @@ sendMessageToHub(agentId, convId, msgText, messageData).then(res => { resolve(res); + deleteMessageDraft(); }).catch(err => { reject(err); }).finally(() => { @@ -647,6 +656,7 @@ } else { sendMessageToHub(agentId, convId, msgText, messageData).then(res => { resolve(res); + deleteMessageDraft(); }).catch(err => { reject(err); }).finally(() => { @@ -717,6 +727,7 @@ /** @param {any} e */ function handleMessageInput(e) { const value = e.target.value; + saveMessageDraft(value); if (!!!_.trim(value)) { return; } @@ -1137,6 +1148,8 @@ isOpenBigMsgModal = !isOpenBigMsgModal; if (!isOpenBigMsgModal) { bigText = ''; + } else { + bigText = text; } } @@ -1320,6 +1333,26 @@ }); } + /** @param {any} e */ + function handleInputBigText(e) { + saveMessageDraft(e.target.value); + } + + const MESSAGE_STORAGE_KEY = 'message_draft_'; + function getMessageDraft() { + return messageStorage.get(MESSAGE_STORAGE_KEY + params.conversationId); + } + + /** + * @param {any} message + */ + function saveMessageDraft(message) { + messageStorage.set(MESSAGE_STORAGE_KEY + params.conversationId, message, 24 * 60 * 60 * 1000); + } + + function deleteMessageDraft() { + messageStorage.remove(MESSAGE_STORAGE_KEY + params.conversationId); + } function handlePaneResize() { const header = document.querySelector('.chat-head'); if (!header) return; @@ -1410,7 +1443,7 @@ cancel={() => toggleBigMessageModal()} disableConfirmBtn={!!!_.trim(bigText)} > -