70cba72a80
Backend:
- caldav.py: Full event lifecycle — update_event, delete_event; VTODO suite —
create_todo, list_todos, complete_todo, delete_todo; list_calendars; timezone
support via ZoneInfo; reminders via VALARM; attendees; multi-calendar search
(_get_all_calendars scans all calendars when no specific one is configured)
- tools.py: New update_note tool (find by title + replace/append modes),
7 new CalDAV tool definitions, corresponding execute_tool cases
- llm.py: Update system prompt — add update_note guidance, full CalDAV action list
- intent.py: Confidence scoring (high/medium/low) + should_execute property;
conversation history support for anaphora resolution; routing rules for
update/delete events, todos, update_note vs create_note disambiguation,
time-period → list_events (not search_events), reminder_minutes conversion
- generation_task.py: Parallel fetch of tools + intent_model setting; dedicated
intent model (OLLAMA_INTENT_MODEL env var or per-user intent_model setting)
- config.py: Add OLLAMA_INTENT_MODEL env var
Frontend:
- HomeView.vue: Inline streaming response (no navigation); quick action chips;
isConversational computed — prominent "Continue this conversation" CTA when
no tool calls; auto-focus chat input on mount via chatInputRef
- DashboardChatInput.vue: defineExpose({ focus }) for external focus control
- ChatView.vue: Escape key handler — close picker → close sidebar → clear
textarea → navigate home; onUnmounted cleanup
- App.vue: Global ? key shortcut toggles keyboard shortcuts overlay; shared
state via useShortcuts composable; Transition animation
- AppHeader.vue: ? button for shortcuts overlay discoverability
- useShortcuts.ts (new): Shared showShortcuts ref + open/close/toggle helpers
- ToolCallCard.vue: note_updated, event_updated, event_deleted, calendars,
todo, todos, todo_completed, todo_deleted label cases + render blocks
- SettingsView.vue: Intent model field + caldav_timezone setting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
245 lines
6.2 KiB
Vue
245 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, onUnmounted, watch } from "vue";
|
|
import AppHeader from "@/components/AppHeader.vue";
|
|
import ToastNotification from "@/components/ToastNotification.vue";
|
|
import { useTheme } from "@/composables/useTheme";
|
|
import { useShortcuts } from "@/composables/useShortcuts";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { useChatStore } from "@/stores/chat";
|
|
import { useSettingsStore } from "@/stores/settings";
|
|
|
|
useTheme();
|
|
|
|
const authStore = useAuthStore();
|
|
const chatStore = useChatStore();
|
|
const settingsStore = useSettingsStore();
|
|
const { showShortcuts, toggleShortcuts, closeShortcuts } = useShortcuts();
|
|
|
|
function startAppServices() {
|
|
chatStore.startStatusPolling();
|
|
settingsStore.fetchSettings();
|
|
}
|
|
|
|
function stopAppServices() {
|
|
chatStore.stopStatusPolling();
|
|
}
|
|
|
|
function isInputActive(): boolean {
|
|
const el = document.activeElement;
|
|
if (!el) return false;
|
|
const tag = (el as HTMLElement).tagName;
|
|
return tag === "INPUT" || tag === "TEXTAREA" || (el as HTMLElement).isContentEditable;
|
|
}
|
|
|
|
function onGlobalKeydown(e: KeyboardEvent) {
|
|
if (!authStore.isAuthenticated) return;
|
|
// ? — toggle shortcuts overlay (only when not typing)
|
|
if (e.key === "?" && !isInputActive() && !e.ctrlKey && !e.metaKey) {
|
|
toggleShortcuts();
|
|
return;
|
|
}
|
|
// Escape — close shortcuts overlay
|
|
if (e.key === "Escape" && showShortcuts.value) {
|
|
closeShortcuts();
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
document.addEventListener("keydown", onGlobalKeydown);
|
|
await authStore.checkAuth();
|
|
if (authStore.isAuthenticated) {
|
|
startAppServices();
|
|
}
|
|
});
|
|
|
|
watch(
|
|
() => authStore.isAuthenticated,
|
|
(authenticated) => {
|
|
if (authenticated) {
|
|
startAppServices();
|
|
} else {
|
|
stopAppServices();
|
|
}
|
|
}
|
|
);
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener("keydown", onGlobalKeydown);
|
|
stopAppServices();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<template v-if="authStore.isAuthenticated">
|
|
<div class="app-shell">
|
|
<AppHeader />
|
|
<div class="app-content">
|
|
<router-view />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keyboard shortcuts overlay -->
|
|
<Transition name="shortcuts-fade">
|
|
<div v-if="showShortcuts" class="shortcuts-overlay" @click.self="closeShortcuts">
|
|
<div class="shortcuts-panel">
|
|
<div class="shortcuts-header">
|
|
<h3>Keyboard Shortcuts</h3>
|
|
<button class="shortcuts-close" @click="closeShortcuts">×</button>
|
|
</div>
|
|
<div class="shortcuts-body">
|
|
<div class="shortcuts-section">
|
|
<div class="shortcuts-section-title">Navigation</div>
|
|
<div class="shortcut-row">
|
|
<span class="shortcut-key">Esc</span>
|
|
<span class="shortcut-desc">Go to home / close dialog</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<span class="shortcut-key">?</span>
|
|
<span class="shortcut-desc">Toggle this shortcuts panel</span>
|
|
</div>
|
|
</div>
|
|
<div class="shortcuts-section">
|
|
<div class="shortcuts-section-title">Chat</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">Enter</kbd>
|
|
<span class="shortcut-desc">Send message</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">Shift</kbd>
|
|
<span class="shortcut-key-sep">+</span>
|
|
<kbd class="shortcut-key">Enter</kbd>
|
|
<span class="shortcut-desc">New line in message</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">Esc</kbd>
|
|
<span class="shortcut-desc">Clear input / go home</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
<template v-else>
|
|
<router-view />
|
|
</template>
|
|
<ToastNotification />
|
|
</template>
|
|
|
|
<style>
|
|
.app-shell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
overflow: hidden;
|
|
}
|
|
.app-shell > .app-header {
|
|
flex-shrink: 0;
|
|
}
|
|
.app-content {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Shortcuts overlay */
|
|
.shortcuts-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--color-overlay, rgba(0, 0, 0, 0.45));
|
|
z-index: 9000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.shortcuts-panel {
|
|
background: var(--color-bg-card);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md, 8px);
|
|
box-shadow: 0 8px 32px var(--color-shadow, rgba(0,0,0,0.2));
|
|
width: min(420px, 92vw);
|
|
overflow: hidden;
|
|
}
|
|
.shortcuts-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.85rem 1rem 0.75rem;
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
.shortcuts-header h3 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--color-text);
|
|
}
|
|
.shortcuts-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.4rem;
|
|
line-height: 1;
|
|
color: var(--color-text-muted);
|
|
cursor: pointer;
|
|
padding: 0 0.25rem;
|
|
}
|
|
.shortcuts-close:hover {
|
|
color: var(--color-text);
|
|
}
|
|
.shortcuts-body {
|
|
padding: 0.75rem 1rem 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
.shortcuts-section-title {
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--color-text-muted);
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
.shortcut-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
padding: 0.2rem 0;
|
|
}
|
|
.shortcut-key {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 1.8rem;
|
|
padding: 0.15rem 0.4rem;
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-bottom-width: 2px;
|
|
border-radius: 4px;
|
|
font-size: 0.78rem;
|
|
font-family: ui-monospace, monospace;
|
|
color: var(--color-text);
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
}
|
|
.shortcut-key-sep {
|
|
font-size: 0.78rem;
|
|
color: var(--color-text-muted);
|
|
}
|
|
.shortcut-desc {
|
|
font-size: 0.875rem;
|
|
color: var(--color-text);
|
|
margin-left: 0.25rem;
|
|
}
|
|
|
|
/* Transition */
|
|
.shortcuts-fade-enter-active,
|
|
.shortcuts-fade-leave-active {
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
.shortcuts-fade-enter-from,
|
|
.shortcuts-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|