Skip to content

Commit be9d637

Browse files
author
roary.yao
committed
AII-426
1 parent c6862dd commit be9d637

File tree

15 files changed

+474
-67
lines changed

15 files changed

+474
-67
lines changed

src/lib/common/InPlaceEdit.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
dispatch('submit', value);
3232
}
3333
}
34-
34+
35+
/** @param {any} event */
36+
function handleInput(event) {
37+
dispatch('input', event);
38+
}
39+
3540
/** @param {any} event */
3641
function keydown(event) {
3742
if (event.key == 'Escape') {
@@ -57,6 +62,7 @@
5762
maxlength={maxLength}
5863
use:focus
5964
on:blur={() => submit()}
65+
on:input={handleInput}
6066
/>
6167
</form>
6268
{:else}

src/lib/common/nav-bar/NavItem.svelte

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
/** @type {() => void} */
6969
export let onDelete = () => {};
7070
71+
/** @type {() => void} */
72+
export let onInput = () => {};
73+
7174
/** @param {any} e */
7275
function handleTabClick(e) {
7376
e.preventDefault();
@@ -79,6 +82,12 @@
7982
e.preventDefault();
8083
onDelete?.();
8184
}
85+
86+
/** @param {any} e */
87+
function handleTabInput(e) {
88+
e.preventDefault();
89+
onInput?.();
90+
}
8291
</script>
8392
8493
<li
@@ -102,7 +111,12 @@
102111
on:click={(e) => handleTabClick(e)}
103112
>
104113
{#if allowEdit}
105-
<InPlaceEdit bind:value={navBtnText} maxLength={maxEditLength} placeholder={editPlaceholder} />
114+
<InPlaceEdit
115+
bind:value={navBtnText}
116+
maxLength={maxEditLength}
117+
placeholder={editPlaceholder}
118+
on:input={handleTabInput}
119+
/>
106120
{:else}
107121
<div style="height: 100%" class="line-align-center">
108122
<div>{navBtnText}</div>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
class LocalStorageManager {
2+
/**
3+
* @param {{ maxSize?: number, overflowStrategy?: 'LRU' | 'EXPIRE_FIRST' }} options
4+
*/
5+
constructor(options = {}) {
6+
this.maxSize = options.maxSize || 4 * 1024 * 1024;
7+
this.overflowStrategy = options.overflowStrategy || 'EXPIRE_FIRST';
8+
}
9+
10+
/**
11+
* @param {string} key
12+
* @param {any} value
13+
* @param {number | null} ttl
14+
*/
15+
set(key, value, ttl = null) {
16+
try {
17+
const item = {
18+
value,
19+
meta: {
20+
expire: ttl ? Date.now() + ttl : null,
21+
lastAccess: Date.now()
22+
}
23+
};
24+
25+
const cost = this._calculateItemCost(key, JSON.stringify(item));
26+
27+
if (cost > this.maxSize) throw new Error('Item exceeds maximum storage size');
28+
29+
if (this._getTotalSize() + cost > this.maxSize) {
30+
this._performCleanup(cost);
31+
}
32+
33+
if (this._getTotalSize() + cost > this.maxSize) throw new Error('Item exceeds maximum storage size');
34+
35+
localStorage.setItem(key, JSON.stringify(item));
36+
this._updateSizeCache(cost);
37+
} catch (/** @type {any} */ error) {
38+
console.error('Storage Error:', error);
39+
}
40+
}
41+
42+
/**
43+
* @param {string} key
44+
* @returns {any}
45+
*/
46+
get(key) {
47+
const raw = localStorage.getItem(key);
48+
if (!raw) return null;
49+
50+
const item = JSON.parse(raw);
51+
if (this._isExpired(item)) {
52+
this.remove(key);
53+
return null;
54+
}
55+
56+
item.meta.lastAccess = Date.now();
57+
localStorage.setItem(key, JSON.stringify(item));
58+
return item.value;
59+
}
60+
61+
/**
62+
* @param {string} key
63+
*/
64+
remove(key) {
65+
const raw = localStorage.getItem(key);
66+
localStorage.removeItem(key);
67+
if (raw) this._updateSizeCache(-this._calculateItemCost(key, raw));
68+
}
69+
70+
clear() {
71+
localStorage.clear();
72+
this._sizeCache = 0;
73+
}
74+
75+
/**
76+
* @param {any} item
77+
* @returns {boolean}
78+
*/
79+
_isExpired(item) {
80+
return item && item.meta && item.meta.expire && item.meta.expire < Date.now();
81+
}
82+
83+
/**
84+
* @param {string} key
85+
* @param {string} valueString
86+
* @returns {number}
87+
*/
88+
_calculateItemCost(key, valueString) {
89+
const encoder = new TextEncoder();
90+
return encoder.encode(key).length + encoder.encode(valueString).length;
91+
}
92+
93+
_getTotalSize() {
94+
if (!this._sizeCache) this._rebuildSizeCache();
95+
return this._sizeCache;
96+
}
97+
98+
_rebuildSizeCache() {
99+
this._sizeCache = Array.from({ length: localStorage.length })
100+
.reduce((total, _, i) => {
101+
const key = localStorage.key(i);
102+
const item = key ? localStorage.getItem(key) : null;
103+
return total + (key && item ? this._calculateItemCost(key, item) : 0);
104+
}, 0);
105+
}
106+
107+
/**
108+
* @param {number} delta
109+
*/
110+
_updateSizeCache(delta) {
111+
this._sizeCache = (this._sizeCache || 0) + delta;
112+
}
113+
114+
/**
115+
* @param {number} requiredSpace
116+
*/
117+
_performCleanup(requiredSpace) {
118+
const /** @type {any[]} */ candidates = [];
119+
120+
Array.from({ length: localStorage.length }).forEach((_, i) => {
121+
const key = localStorage.key(i);
122+
const raw = key ? localStorage.getItem(key) : null;
123+
if (!key || !raw) {
124+
return;
125+
}
126+
const item = JSON.parse(raw);
127+
if (item && item.meta) {
128+
candidates.push({
129+
key,
130+
size: this._calculateItemCost(key, raw),
131+
expire: item.meta.expire || Infinity,
132+
lastAccess: item.meta.lastAccess
133+
});
134+
}
135+
});
136+
137+
switch (this.overflowStrategy) {
138+
case 'EXPIRE_FIRST':
139+
candidates.sort((a, b) => a.expire - b.expire);
140+
break;
141+
case 'LRU':
142+
candidates.sort((a, b) => a.lastAccess - b.lastAccess);
143+
break;
144+
}
145+
146+
let freedSpace = 0;
147+
while (freedSpace < requiredSpace && candidates.length > 0) {
148+
const target = candidates.shift();
149+
this.remove(target.key);
150+
freedSpace += target.size;
151+
}
152+
}
153+
}
154+
155+
export default LocalStorageManager;

src/lib/langs/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,5 +300,6 @@
300300
"Account Origin":"Account Origin",
301301
"Update Date":"Update Date",
302302
"Create Date":"Create Date",
303-
"Active now":"Active now"
303+
"Active now":"Active now",
304+
"Reset":"Reset"
304305
}

src/lib/langs/zh.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,5 +495,6 @@
495495
"Account Origin":"账户起源",
496496
"Update Date":"更新日期",
497497
"Create Date":"创建日期",
498-
"Active now":"正在工作"
498+
"Active now":"正在工作",
499+
"Reset":"重置"
499500
}

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import ChatBigMessage from './chat-util/chat-big-message.svelte';
7171
import PersistLog from './persist-log/persist-log.svelte';
7272
import InstantLog from './instant-log/instant-log.svelte';
73+
import LocalStorageManager from '$lib/helpers/utils/storage-manager';
7374
7475
7576
const options = {
@@ -96,6 +97,8 @@
9697
/** @type {import('$userTypes').UserModel} */
9798
export let currentUser;
9899
100+
const messageStorage = new LocalStorageManager();
101+
99102
/** @type {string} */
100103
let text = '';
101104
let editText = '';
@@ -219,6 +222,10 @@
219222
selectedTags = conversation?.tags || [];
220223
initUserSentMessages(dialogs);
221224
initChatView();
225+
const messageDraft = getMessageDraft();
226+
if (messageDraft) {
227+
text = messageDraft;
228+
}
222229
223230
signalr.onMessageReceivedFromClient = onMessageReceivedFromClient;
224231
signalr.onMessageReceivedFromCsr = onMessageReceivedFromClient;
@@ -611,6 +618,7 @@
611618
612619
sendMessageToHub(agentId, convId, msgText, messageData).then(res => {
613620
resolve(res);
621+
deleteMessageDraft();
614622
}).catch(err => {
615623
reject(err);
616624
}).finally(() => {
@@ -636,6 +644,7 @@
636644
637645
sendMessageToHub(agentId, convId, msgText, messageData).then(res => {
638646
resolve(res);
647+
deleteMessageDraft();
639648
}).catch(err => {
640649
reject(err);
641650
}).finally(() => {
@@ -645,6 +654,7 @@
645654
} else {
646655
sendMessageToHub(agentId, convId, msgText, messageData).then(res => {
647656
resolve(res);
657+
deleteMessageDraft();
648658
}).catch(err => {
649659
reject(err);
650660
}).finally(() => {
@@ -715,6 +725,7 @@
715725
/** @param {any} e */
716726
function handleMessageInput(e) {
717727
const value = e.target.value;
728+
saveMessageDraft(value);
718729
if (!!!_.trim(value)) {
719730
return;
720731
}
@@ -1135,6 +1146,8 @@
11351146
isOpenBigMsgModal = !isOpenBigMsgModal;
11361147
if (!isOpenBigMsgModal) {
11371148
bigText = '';
1149+
} else {
1150+
bigText = text;
11381151
}
11391152
}
11401153
@@ -1317,6 +1330,27 @@
13171330
isLoading = false;
13181331
});
13191332
}
1333+
1334+
/** @param {any} e */
1335+
function handleInputBigText(e) {
1336+
saveMessageDraft(e.target.value);
1337+
}
1338+
1339+
const MESSAGE_STORAGE_KEY = 'message_draft_';
1340+
function getMessageDraft() {
1341+
return messageStorage.get(MESSAGE_STORAGE_KEY + params.conversationId);
1342+
}
1343+
1344+
/**
1345+
* @param {any} message
1346+
*/
1347+
function saveMessageDraft(message) {
1348+
messageStorage.set(MESSAGE_STORAGE_KEY + params.conversationId, message, 24 * 60 * 60 * 1000);
1349+
}
1350+
1351+
function deleteMessageDraft() {
1352+
messageStorage.remove(MESSAGE_STORAGE_KEY + params.conversationId);
1353+
}
13201354
</script>
13211355
13221356
@@ -1400,7 +1434,7 @@
14001434
cancel={() => toggleBigMessageModal()}
14011435
disableConfirmBtn={!!!_.trim(bigText)}
14021436
>
1403-
<textarea class="form-control chat-input" rows="25" maxlength={maxTextLength} bind:value={bigText} placeholder="Enter Message..." />
1437+
<textarea class="form-control chat-input" rows="25" maxlength={maxTextLength} bind:value={bigText} placeholder="Enter Message..." on:input={handleInputBigText} />
14041438
<div class="text-secondary text-end text-count">
14051439
<div>{`${(bigText?.length || 0)}/${maxTextLength}`}</div>
14061440
</div>
@@ -1840,12 +1874,10 @@
18401874
</ChatFileUploader>
18411875
</ChatTextArea>
18421876
<div class="chat-util-links">
1843-
{#if !isLite}
1844-
<ChatBigMessage
1845-
disabled={isSendingMsg || isThinking || disableAction}
1846-
on:click={() => toggleBigMessageModal()}
1847-
/>
1848-
{/if}
1877+
<ChatBigMessage
1878+
disabled={isSendingMsg || isThinking || disableAction}
1879+
on:click={() => toggleBigMessageModal()}
1880+
/>
18491881
{#if PUBLIC_LIVECHAT_FILES_ENABLED === 'true'}
18501882
<ChatUtil disabled={disableAction} on:click={() => loadChatUtils = true} />
18511883
{/if}

0 commit comments

Comments
 (0)