Skip to content

Commit 76fe7c2

Browse files
author
Jicheng Lu
committed
Merge branch 'main' of https://github.com/SciSharp/BotSharp-UI into features/add-agent-rule-action
2 parents c4f5dbf + f92b99b commit 76fe7c2

13 files changed

Lines changed: 510 additions & 59 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
import { Button, Input } from '@sveltestrap/sveltestrap';
4+
import Flatpickr from 'svelte-flatpickr';
5+
import 'flatpickr/dist/flatpickr.css';
6+
import { TIME_RANGE_OPTIONS, CUSTOM_DATE_RANGE } from '$lib/helpers/constants';
7+
import { clickoutsideDirective } from '$lib/helpers/directives';
8+
9+
/** @type {string} */
10+
export let timeRange = '';
11+
12+
/** @type {string} */
13+
export let startDate = '';
14+
15+
/** @type {string} */
16+
export let endDate = '';
17+
18+
/** @type {string} */
19+
let timeRangeDisplayText = '';
20+
21+
/** @type {boolean} */
22+
let showDatePicker = false;
23+
24+
/** @type {HTMLDivElement | null} */
25+
let datePickerRef = null;
26+
27+
/** @type {string} */
28+
let datePickerTab = 'relative';
29+
30+
// Temporary dates for custom selection (before confirming)
31+
/** @type {string} */
32+
let tempStartDate = '';
33+
34+
/** @type {string} */
35+
let tempEndDate = '';
36+
37+
// Flatpickr instance reference
38+
/** @type {any} */
39+
let flatpickrInstance = null;
40+
41+
// Format date for flatpickr (Date object to YYYY-MM-DD string)
42+
/** @param {Date} date */
43+
function formatDateForFlatpickr(/** @type {Date} */ date) {
44+
if (!date) return '';
45+
const year = date.getFullYear();
46+
const month = String(date.getMonth() + 1).padStart(2, '0');
47+
const day = String(date.getDate()).padStart(2, '0');
48+
return `${year}-${month}-${day}`;
49+
}
50+
51+
// Flatpickr options for date range selection
52+
/** @type {any} */
53+
let flatpickrOptions = {
54+
mode: 'range',
55+
dateFormat: 'Y-m-d',
56+
inline: true,
57+
allowInput: false,
58+
onChange: (/** @type {Date[]} */ selectedDates, /** @type {string} */ dateStr, /** @type {any} */ instance) => {
59+
if (selectedDates.length === 1) {
60+
// Only start date selected
61+
tempStartDate = formatDateForFlatpickr(selectedDates[0]);
62+
tempEndDate = '';
63+
} else if (selectedDates.length === 2) {
64+
// Both dates selected
65+
tempStartDate = formatDateForFlatpickr(selectedDates[0]);
66+
tempEndDate = formatDateForFlatpickr(selectedDates[1]);
67+
}
68+
}
69+
};
70+
71+
// Handle manual input changes
72+
function handleStartDateChange() {
73+
if (tempStartDate && flatpickrInstance) {
74+
if (tempEndDate) {
75+
flatpickrInstance.setDate([tempStartDate, tempEndDate], false);
76+
} else {
77+
flatpickrInstance.setDate([tempStartDate], false);
78+
}
79+
}
80+
}
81+
82+
function handleEndDateChange() {
83+
if (tempStartDate && tempEndDate && flatpickrInstance) {
84+
flatpickrInstance.setDate([tempStartDate, tempEndDate], false);
85+
}
86+
}
87+
88+
// Initialize temp dates when opening custom tab
89+
function initCustomDates() {
90+
if (startDate) {
91+
tempStartDate = startDate;
92+
}
93+
if (endDate) {
94+
tempEndDate = endDate;
95+
}
96+
if (!tempStartDate && !tempEndDate) {
97+
tempStartDate = getYesterdayStr();
98+
tempEndDate = getTodayStr();
99+
}
100+
// Update flatpickr with initial dates
101+
if (flatpickrInstance) {
102+
if (tempStartDate && tempEndDate) {
103+
flatpickrInstance.setDate([tempStartDate, tempEndDate], false);
104+
} else if (tempStartDate) {
105+
flatpickrInstance.setDate([tempStartDate], false);
106+
}
107+
}
108+
}
109+
110+
// Preset time range options
111+
const presetTimeRangeOptions = TIME_RANGE_OPTIONS.map(x => ({
112+
label: x.label,
113+
value: x.value
114+
}));
115+
116+
// Get today's date in YYYY-MM-DD format
117+
const getTodayStr = () => {
118+
const d = new Date();
119+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
120+
};
121+
122+
// Get yesterday's date in YYYY-MM-DD format
123+
const getYesterdayStr = () => {
124+
const d = new Date();
125+
d.setDate(d.getDate() - 1);
126+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
127+
};
128+
129+
// Format date for display (YYYY-MM-DD -> MM/DD/YYYY)
130+
const formatDateForDisplay = (/** @type {string} */ dateStr) => {
131+
if (!dateStr) return '';
132+
const [year, month, day] = dateStr.split('-');
133+
return `${month}/${day}/${year}`;
134+
};
135+
136+
// Update time range display text reactively
137+
$: {
138+
if (timeRange === CUSTOM_DATE_RANGE && startDate && endDate) {
139+
const start = formatDateForDisplay(startDate);
140+
const end = formatDateForDisplay(endDate);
141+
if (start === end) {
142+
timeRangeDisplayText = start;
143+
} else {
144+
timeRangeDisplayText = `${start} - ${end}`;
145+
}
146+
} else if (timeRange === CUSTOM_DATE_RANGE) {
147+
timeRangeDisplayText = 'Custom';
148+
} else {
149+
const selected = presetTimeRangeOptions.find(x => x.value === timeRange);
150+
timeRangeDisplayText = selected ? selected.label : '';
151+
}
152+
}
153+
154+
const dispatch = createEventDispatcher();
155+
156+
/** @param {string} optionValue */
157+
function handleRelativeOptionClick(/** @type {string} */ optionValue) {
158+
timeRange = optionValue;
159+
startDate = '';
160+
endDate = '';
161+
showDatePicker = false;
162+
dispatch('change', { timeRange, startDate, endDate });
163+
}
164+
165+
function handleApply() {
166+
if (tempStartDate) {
167+
const finalEndDate = tempEndDate || tempStartDate;
168+
// Update props through binding (will trigger reactivity)
169+
startDate = tempStartDate;
170+
endDate = finalEndDate;
171+
timeRange = CUSTOM_DATE_RANGE;
172+
// Dispatch change event with updated values
173+
dispatch('change', {
174+
timeRange: CUSTOM_DATE_RANGE,
175+
startDate: tempStartDate,
176+
endDate: finalEndDate
177+
});
178+
}
179+
showDatePicker = false;
180+
}
181+
182+
function handleCancel() {
183+
showDatePicker = false;
184+
}
185+
</script>
186+
187+
<div class="position-relative">
188+
<button
189+
type="button"
190+
class="form-control text-start d-flex align-items-center justify-content-between"
191+
on:click={() => {
192+
showDatePicker = !showDatePicker;
193+
if (showDatePicker) {
194+
// If custom date is selected, switch to custom tab; otherwise use relative tab
195+
datePickerTab = timeRange === CUSTOM_DATE_RANGE ? 'custom' : 'relative';
196+
if (datePickerTab === 'custom') {
197+
// Delay init to ensure flatpickr is mounted
198+
setTimeout(() => {
199+
initCustomDates();
200+
}, 0);
201+
}
202+
}
203+
}}
204+
style="cursor: pointer;"
205+
>
206+
<span>{timeRangeDisplayText || 'Select time range'}</span>
207+
<i class="bx bx-chevron-down"></i>
208+
</button>
209+
{#if showDatePicker}
210+
<div
211+
bind:this={datePickerRef}
212+
use:clickoutsideDirective
213+
on:clickoutside={(/** @type {any} */ e) => {
214+
if (e.detail && e.detail.targetNode && datePickerRef) {
215+
if (!datePickerRef.contains(e.detail.targetNode)) {
216+
showDatePicker = false;
217+
}
218+
}
219+
}}
220+
class="position-absolute top-100 start-0 mt-1 bg-white border rounded shadow-lg"
221+
style="z-index: 1050; min-width: 320px; max-width: 350px;"
222+
>
223+
<ul class="nav nav-tabs border-bottom mb-0 px-2 pt-2" role="tablist">
224+
<li class="nav-item flex-fill" role="presentation">
225+
<button
226+
class="nav-link fw-semibold {datePickerTab === 'relative' ? 'active text-primary' : 'text-muted'}"
227+
type="button"
228+
role="tab"
229+
style="padding: 0.5rem 0.75rem; {datePickerTab === 'relative' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
230+
on:click={() => datePickerTab = 'relative'}
231+
>
232+
Relative
233+
</button>
234+
</li>
235+
<li class="nav-item flex-fill" role="presentation">
236+
<button
237+
class="nav-link fw-semibold {datePickerTab === 'custom' ? 'active text-primary' : 'text-muted'}"
238+
type="button"
239+
role="tab"
240+
style="padding: 0.5rem 0.75rem; {datePickerTab === 'custom' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
241+
on:click={() => {
242+
datePickerTab = 'custom';
243+
// Delay init to ensure flatpickr is mounted
244+
setTimeout(() => {
245+
initCustomDates();
246+
}, 0);
247+
}}
248+
>
249+
Custom
250+
</button>
251+
</li>
252+
</ul>
253+
254+
<div class="p-4">
255+
{#if datePickerTab === 'relative'}
256+
<div class="d-flex flex-column gap-2" style="max-height: 300px; overflow-y: auto;">
257+
{#each presetTimeRangeOptions as option}
258+
<button
259+
type="button"
260+
class="btn btn-sm btn-outline-secondary text-start {timeRange === option.value ? 'active' : ''}"
261+
on:click={(e) => {
262+
e.preventDefault();
263+
e.stopPropagation();
264+
handleRelativeOptionClick(option.value);
265+
}}
266+
>
267+
{option.label}
268+
</button>
269+
{/each}
270+
</div>
271+
{:else if datePickerTab === 'custom'}
272+
<!-- Calendar Grid -->
273+
<div class="mb-3">
274+
<Flatpickr
275+
options={flatpickrOptions}
276+
bind:flatpickr={flatpickrInstance}
277+
/>
278+
</div>
279+
280+
<!-- Date Input Fields -->
281+
<div class="d-flex align-items-center gap-2 mb-3">
282+
<Input
283+
type="date"
284+
bind:value={tempStartDate}
285+
class="form-control form-control-sm"
286+
on:change={handleStartDateChange}
287+
/>
288+
<span class="text-muted small">to</span>
289+
<Input
290+
type="date"
291+
bind:value={tempEndDate}
292+
class="form-control form-control-sm"
293+
on:change={handleEndDateChange}
294+
/>
295+
</div>
296+
297+
<div class="d-flex justify-content-end gap-2 mt-3">
298+
<Button
299+
color="secondary"
300+
size="sm"
301+
type="button"
302+
on:click={(e) => {
303+
e.preventDefault();
304+
e.stopPropagation();
305+
handleCancel();
306+
}}
307+
>
308+
Cancel
309+
</Button>
310+
<Button
311+
color="primary"
312+
size="sm"
313+
type="button"
314+
on:click={(e) => {
315+
e.preventDefault();
316+
e.stopPropagation();
317+
handleApply();
318+
}}
319+
>
320+
Apply
321+
</Button>
322+
</div>
323+
{/if}
324+
</div>
325+
</div>
326+
{/if}
327+
</div>
328+
329+
<style>
330+
/* Hide flatpickr's default input field when using inline mode */
331+
:global(.flatpickr-input) {
332+
display: none !important;
333+
}
334+
</style>

src/lib/helpers/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export const IMAGE_DATA_PREFIX = 'data:image';
4949
export const INTEGER_REGEX = "[0-9]+";
5050
export const DECIMAL_REGEX = "[0-9.]+";
5151

52+
// Custom date range identifier (not in TimeRange enum)
53+
export const CUSTOM_DATE_RANGE = "Custom date";
54+
5255
export const TIME_RANGE_OPTIONS = [
5356
{ label: TimeRange.Last15Minutes, value: TimeRange.Last15Minutes, qty: 15, unit: 'minutes' },
5457
{ label: TimeRange.Last30Minutes, value: TimeRange.Last30Minutes, qty: 30, unit: 'minutes' },

src/lib/helpers/store.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,14 +241,14 @@ export const conversationSearchOptionStore = createConversationSearchOptionStore
241241
const createConversationUserMessageStore = () => {
242242
return {
243243
reset: () => {
244-
localStorage.removeItem(conversationUserMessageKey);
244+
sessionStorage.removeItem(conversationUserMessageKey);
245245
},
246246
get: () => {
247-
const json = localStorage.getItem(conversationUserMessageKey);
247+
const json = sessionStorage.getItem(conversationUserMessageKey);
248248
return json ? JSON.parse(json) : {};
249249
},
250250
put: (value) => {
251-
localStorage.setItem(conversationUserMessageKey, JSON.stringify(value));
251+
sessionStorage.setItem(conversationUserMessageKey, JSON.stringify(value));
252252
}
253253
}
254254
};

src/lib/helpers/types/conversationTypes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ IRichContent.prototype.language;
324324
* @property {UserStateDetailModel[]} states
325325
* @property {string[]} tags
326326
* @property {string?} [timeRange]
327+
* @property {string} [startDate] - When timeRange is "Custom date", start date in YYYY-MM-DD format (e.g. 2026-01-25)
328+
* @property {string} [endDate] - When timeRange is "Custom date", end date in YYYY-MM-DD format (e.g. 2026-01-30). Defaults to startDate if not provided
327329
*/
328330

329331
/**

0 commit comments

Comments
 (0)