fix: KnowledgeView infinite scroll root + calendar event refresh
- KnowledgeView: IntersectionObserver was watching the viewport instead of the card-grid scroll container, causing infinite scroll to stop loading after only ~29 items. Pass card-grid element as `root`. - CalendarView: listen for 'fable:calendar-changed' custom event and call refetchEvents() so tool-created events appear without navigation. - ChatPanel: dispatch 'fable:calendar-changed' when create_event, update_event, or delete_event tool calls succeed. - tools.py: normalize naive datetimes to UTC before storing events so timezone comparisons in list_events queries are always consistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,19 @@ function scrollToBottom() {
|
|||||||
watch(() => store.streamingContent, () => scrollToBottom())
|
watch(() => store.streamingContent, () => scrollToBottom())
|
||||||
watch(() => store.currentConversation?.messages.length, () => scrollToBottom())
|
watch(() => store.currentConversation?.messages.length, () => scrollToBottom())
|
||||||
|
|
||||||
|
// Notify CalendarView when an event is created/updated/deleted via tool
|
||||||
|
const _calendarToolNames = new Set(['create_event', 'update_event', 'delete_event'])
|
||||||
|
const _seenCalendarToolIdx = new Set<number>()
|
||||||
|
watch(() => store.streamingToolCalls, (calls) => {
|
||||||
|
calls.forEach((tc, i) => {
|
||||||
|
if (!_seenCalendarToolIdx.has(i) && _calendarToolNames.has(tc.function) && tc.status === 'success') {
|
||||||
|
_seenCalendarToolIdx.add(i)
|
||||||
|
document.dispatchEvent(new CustomEvent('fable:calendar-changed'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
watch(() => store.streaming, (s) => { if (!s) _seenCalendarToolIdx.clear() })
|
||||||
|
|
||||||
// ── RAG scope chip (full, non-briefing, non-workspace) ────────────────────────
|
// ── RAG scope chip (full, non-briefing, non-workspace) ────────────────────────
|
||||||
const projects = ref<{ id: number; title: string }[]>([])
|
const projects = ref<{ id: number; title: string }[]>([])
|
||||||
const scopeDropdownOpen = ref(false)
|
const scopeDropdownOpen = ref(false)
|
||||||
|
|||||||
@@ -115,8 +115,18 @@ function onDocClick(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => document.addEventListener("mousedown", onDocClick));
|
function onCalendarChanged() {
|
||||||
onUnmounted(() => document.removeEventListener("mousedown", onDocClick));
|
calendarRef.value?.getApi().refetchEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("mousedown", onDocClick);
|
||||||
|
document.addEventListener("fable:calendar-changed", onCalendarChanged);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("mousedown", onDocClick);
|
||||||
|
document.removeEventListener("fable:calendar-changed", onCalendarChanged);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Calendar callbacks ─────────────────────────────────────────────────────
|
// ── Calendar callbacks ─────────────────────────────────────────────────────
|
||||||
async function loadEvents(
|
async function loadEvents(
|
||||||
|
|||||||
@@ -333,11 +333,12 @@ function formatDate(iso: string): string {
|
|||||||
// ─── Infinite scroll ──────────────────────────────────────────────────────────
|
// ─── Infinite scroll ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const sentinelEl = ref<HTMLElement | null>(null);
|
const sentinelEl = ref<HTMLElement | null>(null);
|
||||||
|
const cardGridEl = ref<HTMLElement | null>(null);
|
||||||
let scrollObserver: IntersectionObserver | null = null;
|
let scrollObserver: IntersectionObserver | null = null;
|
||||||
|
|
||||||
function setupScrollObserver() {
|
function setupScrollObserver() {
|
||||||
if (scrollObserver) scrollObserver.disconnect();
|
if (scrollObserver) scrollObserver.disconnect();
|
||||||
if (!sentinelEl.value) return;
|
if (!sentinelEl.value || !cardGridEl.value) return;
|
||||||
scrollObserver = new IntersectionObserver(
|
scrollObserver = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting && !loading.value && items.value.length < total.value) {
|
if (entries[0].isIntersecting && !loading.value && items.value.length < total.value) {
|
||||||
@@ -345,7 +346,7 @@ function setupScrollObserver() {
|
|||||||
fetchItems();
|
fetchItems();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ rootMargin: "200px" }
|
{ root: cardGridEl.value, rootMargin: "200px" }
|
||||||
);
|
);
|
||||||
scrollObserver.observe(sentinelEl.value);
|
scrollObserver.observe(sentinelEl.value);
|
||||||
}
|
}
|
||||||
@@ -368,7 +369,7 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (sentinelEl.value) setupScrollObserver();
|
if (sentinelEl.value && cardGridEl.value) setupScrollObserver();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -471,7 +472,7 @@ watchEffect(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card grid -->
|
<!-- Card grid -->
|
||||||
<div v-else class="card-grid">
|
<div v-else ref="cardGridEl" class="card-grid">
|
||||||
<div
|
<div
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, timezone
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
from fabledassistant.services.caldav import (
|
from fabledassistant.services.caldav import (
|
||||||
@@ -1409,6 +1409,8 @@ async def execute_tool(
|
|||||||
start_str = f"{start_str}T00:00:00"
|
start_str = f"{start_str}T00:00:00"
|
||||||
try:
|
try:
|
||||||
start_dt = datetime.fromisoformat(start_str)
|
start_dt = datetime.fromisoformat(start_str)
|
||||||
|
if start_dt.tzinfo is None:
|
||||||
|
start_dt = start_dt.replace(tzinfo=timezone.utc)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return {"success": False, "error": f"Invalid start datetime: {start_str!r}"}
|
return {"success": False, "error": f"Invalid start datetime: {start_str!r}"}
|
||||||
end_dt = None
|
end_dt = None
|
||||||
@@ -1417,6 +1419,8 @@ async def execute_tool(
|
|||||||
end_str = f"{end_str}T00:00:00"
|
end_str = f"{end_str}T00:00:00"
|
||||||
try:
|
try:
|
||||||
end_dt = datetime.fromisoformat(end_str)
|
end_dt = datetime.fromisoformat(end_str)
|
||||||
|
if end_dt.tzinfo is None:
|
||||||
|
end_dt = end_dt.replace(tzinfo=timezone.utc)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return {"success": False, "error": f"Invalid end datetime: {end_str!r}"}
|
return {"success": False, "error": f"Invalid end datetime: {end_str!r}"}
|
||||||
project_id = None
|
project_id = None
|
||||||
|
|||||||
Reference in New Issue
Block a user