Skip to content

Commit 9c2c7f6

Browse files
authored
Merge pull request #304 from Roaryyyy/roary-features/chat-window
Roary: AII-426 & AII-449
2 parents 28ff75c + 50fc013 commit 9c2c7f6

File tree

21 files changed

+693
-104
lines changed

21 files changed

+693
-104
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}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script>
2+
import { Input, Dropdown, DropdownMenu, DropdownItem, Spinner, DropdownToggle } from '@sveltestrap/sveltestrap';
3+
import { debounce } from 'lodash';
4+
5+
/** @type {{id: string, name: string} | null} */
6+
export let selectedValue;
7+
export let disabled = false;
8+
export let placeholder = '';
9+
/**
10+
11+
* @type {(arg0: any) => any[] | PromiseLike<any[]>}
12+
*/
13+
export let onSearch;
14+
export let loading = false;
15+
16+
/** @type {any[]} */
17+
let searchResults = [];
18+
let isOpen = false;
19+
20+
// @ts-ignore
21+
const debouncedSearch = debounce(async (query) => {
22+
if (query.length) {
23+
loading = true;
24+
searchResults = await onSearch(query);
25+
loading = false;
26+
isOpen = true;
27+
} else {
28+
searchResults = [];
29+
isOpen = false;
30+
}
31+
}, 500);
32+
33+
/**
34+
* @param {any} e
35+
*/
36+
async function handleInput(e) {
37+
const query = e.target.value;
38+
selectedValue = { id: query, name: query };
39+
await debouncedSearch(query);
40+
}
41+
42+
/**
43+
* @param {{ id: string; name: string; }} result
44+
*/
45+
function selectResult(result) {
46+
selectedValue = result;
47+
}
48+
49+
export function clearSearchResults() {
50+
searchResults = [];
51+
}
52+
53+
</script>
54+
55+
<div class="position-relative">
56+
<Dropdown class="scrollable-dropdown" isOpen={isOpen && (searchResults.length > 0 || loading)} toggle={() => isOpen = !isOpen}>
57+
<DropdownToggle tag="div">
58+
<Input
59+
type="text"
60+
value={selectedValue?.name}
61+
on:input={handleInput}
62+
{disabled}
63+
{placeholder}
64+
/>
65+
</DropdownToggle>
66+
<DropdownMenu class="w-100">
67+
{#if loading}
68+
<DropdownItem>
69+
<Spinner size="sm" />
70+
</DropdownItem>
71+
{:else}
72+
{#each searchResults as result, index}
73+
<DropdownItem
74+
active={selectedValue?.id === result.id}
75+
on:click={() => selectResult(result)}
76+
title={result.name}
77+
>
78+
{result.name}
79+
</DropdownItem>
80+
{/each}
81+
{/if}
82+
</DropdownMenu>
83+
</Dropdown>
84+
</div>

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>

src/lib/helpers/http.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ function skipLoader(config) {
9090
new RegExp('http(s*)://(.*?)/role/(.*?)/details', 'g'),
9191
new RegExp('http(s*)://(.*?)/user/(.*?)/details', 'g'),
9292
new RegExp('http(s*)://(.*?)/agent/labels', 'g'),
93+
new RegExp('http(s*)://(.*?)/conversation/state-search', 'g'),
9394
];
9495

9596
if (config.method === 'post' && postRegexes.some(regex => regex.test(config.url || ''))) {
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/lib/scss/custom/common/_common.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,15 @@ button:focus {
180180
.thin-scrollbar {
181181
scrollbar-width: thin;
182182
}
183+
184+
.scrollable-dropdown {
185+
.dropdown-menu {
186+
max-height: 200px;
187+
overflow-y: auto;
188+
}
189+
.dropdown-item {
190+
white-space: nowrap;
191+
overflow: hidden;
192+
text-overflow: ellipsis;
193+
}
194+
}

src/lib/services/api-endpoints.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export const endpoints = {
6767
conversationTagsUpdateUrl: `${host}/conversation/{conversationId}/update-tags`,
6868
fileUploadUrl: `${host}/agent/{agentId}/conversation/{conversationId}/upload`,
6969
pinConversationUrl: `${host}/agent/{agentId}/conversation/{conversationId}/dashboard`,
70+
conversationStateValueUrl: `${host}/conversation/state-search`,
71+
conversationStateKeyListUrl: `${host}/conversation/state-key`,
7072

7173
// LLM provider
7274
llmProvidersUrl: `${host}/llm-providers`,

src/lib/services/conversation-service.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,38 @@ export async function getAddressOptions(text) {
288288
}
289289
});
290290
return response.data;
291+
}
292+
293+
/**
294+
* get conversation state key list
295+
* @returns {Promise<{id: string, name: string, description: string}[]>}
296+
*/
297+
export async function getConversationStateKey() {
298+
let url = endpoints.conversationStateKeyListUrl;
299+
const response = await axios.get(url);
300+
return response.data;
301+
}
302+
303+
/** @type {import('axios').CancelTokenSource | null} */
304+
let getConversationStateValueCancelToken = null;
305+
/**
306+
* get conversation state value
307+
* @param {string} key
308+
* @param {string} query
309+
* @returns {Promise<{id: string, name: string}[]>}
310+
*/
311+
export async function getConversationStateValue(key, query) {
312+
let url = endpoints.conversationStateValueUrl;
313+
if (getConversationStateValueCancelToken) {
314+
getConversationStateValueCancelToken.cancel();
315+
}
316+
getConversationStateValueCancelToken = axios.CancelToken.source();
317+
const response = await axios.get(url, {
318+
params: {
319+
conversatinFilterType: key,
320+
searchKey: query
321+
},
322+
cancelToken: getConversationStateValueCancelToken.token
323+
});
324+
return response.data;
291325
}

0 commit comments

Comments
 (0)