Add CalDAV calendar integration, LLM-suggested tags, and settings refinements

- CalDAV integration: per-user calendar config, create/list/search events
  via caldav library, LLM tools for calendar operations from chat
- LLM-suggested tags: new tag_suggestions service prompts LLM with existing
  tags and note content to suggest 3-5 relevant tags; exposed via API
  endpoints (suggest-tags, append-tag); integrated into editor views
  (suggest button + clickable pills) and chat tool calls (pills in
  ToolCallCard with one-click apply)
- Settings/model UI refinements, generation task improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 22:40:20 -05:00
parent 8996b45e50
commit d7bc3f3222
22 changed files with 1158 additions and 837 deletions
+1 -35
View File
@@ -6,7 +6,6 @@ import { useSettingsStore } from "@/stores/settings";
import { apiGet } from "@/api/client";
import { renderMarkdown } from "@/utils/markdown";
import ChatMessage from "@/components/ChatMessage.vue";
import ModelSelector from "@/components/ModelSelector.vue";
import ToolCallCard from "@/components/ToolCallCard.vue";
import type { Note } from "@/types/note";
@@ -30,9 +29,6 @@ const noteSearchResults = ref<{ id: number; title: string }[]>([]);
const noteSearchLoading = ref(false);
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null;
// Model selection
const selectedModel = ref("");
// Exclude tracking (session-scoped per conversation)
const excludedNoteIds = ref<Set<number>>(new Set());
@@ -76,12 +72,6 @@ onMounted(async () => {
await store.fetchConversations();
if (convId.value) {
await store.fetchConversation(convId.value);
if (store.currentConversation?.model) {
selectedModel.value = store.currentConversation.model;
}
}
if (!selectedModel.value) {
selectedModel.value = store.defaultModel;
}
nextTick(() => inputEl.value?.focus());
});
@@ -112,26 +102,6 @@ watch(convId, async (newId) => {
nextTick(() => inputEl.value?.focus());
});
// Sync selectedModel from loaded conversation
watch(
() => store.currentConversation?.model,
(model) => {
if (model) selectedModel.value = model;
}
);
// Persist model changes to backend
watch(selectedModel, (newModel, oldModel) => {
if (
newModel &&
oldModel &&
newModel !== oldModel &&
store.currentConversation
) {
store.updateConversationModel(store.currentConversation.id, newModel);
}
});
watch(
() => store.streamingContent,
() => scrollToBottom()
@@ -155,7 +125,7 @@ async function selectConversation(id: number) {
}
async function newConversation() {
const conv = await store.createConversation("", selectedModel.value);
const conv = await store.createConversation();
await store.fetchConversation(conv.id);
router.push(`/chat/${conv.id}`);
}
@@ -344,10 +314,6 @@ function excludeAutoNote(noteId: number) {
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<ModelSelector
v-model="selectedModel"
:disabled="store.streaming"
/>
<h2>{{ store.currentConversation.title || "New Chat" }}</h2>
<button
v-if="store.currentConversation.messages.length"