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 -51
View File
@@ -13,9 +13,7 @@ import type {
ConversationDetail,
ContextMeta,
Message,
OllamaModel,
OllamaStatus,
RunningModel,
SendMessageResponse,
ToolCallRecord,
} from "@/types/chat";
@@ -29,8 +27,6 @@ export const useChatStore = defineStore("chat", () => {
const streamingContent = ref("");
const streamingToolCalls = ref<ToolCallRecord[]>([]);
const lastContextMeta = ref<ContextMeta | null>(null);
const models = ref<OllamaModel[]>([]);
const runningModels = ref<RunningModel[]>([]);
const ollamaStatus = ref<"checking" | "available" | "unavailable">("checking");
const modelStatus = ref<"checking" | "ready" | "not_found">("checking");
const defaultModel = ref("");
@@ -56,14 +52,10 @@ export const useChatStore = defineStore("chat", () => {
}
}
async function createConversation(
title = "",
model = ""
): Promise<Conversation> {
async function createConversation(title = ""): Promise<Conversation> {
try {
const conv = await apiPost<Conversation>("/api/chat/conversations", {
title,
model,
});
conversations.value.unshift(conv);
return conv;
@@ -319,24 +311,6 @@ export const useChatStore = defineStore("chat", () => {
}
}
async function fetchModels() {
try {
const data = await apiGet<{ models: OllamaModel[] }>("/api/chat/models");
models.value = data.models;
} catch {
models.value = [];
}
}
async function fetchRunningModels() {
try {
const data = await apiGet<{ models: RunningModel[] }>("/api/chat/ps");
runningModels.value = data.models;
} catch {
runningModels.value = [];
}
}
async function warmModel(model: string) {
try {
await apiPost("/api/chat/warm", { model });
@@ -345,25 +319,6 @@ export const useChatStore = defineStore("chat", () => {
}
}
async function updateConversationModel(id: number, model: string) {
try {
const updated = await apiPatch<Conversation>(
`/api/chat/conversations/${id}`,
{ model }
);
const idx = conversations.value.findIndex((c) => c.id === id);
if (idx !== -1) {
conversations.value[idx] = { ...conversations.value[idx], ...updated };
}
if (currentConversation.value?.id === id) {
currentConversation.value.model = updated.model;
}
} catch (e) {
useToastStore().show("Failed to update model", "error");
throw e;
}
}
return {
conversations,
currentConversation,
@@ -373,8 +328,6 @@ export const useChatStore = defineStore("chat", () => {
streamingContent,
streamingToolCalls,
lastContextMeta,
models,
runningModels,
ollamaStatus,
modelStatus,
defaultModel,
@@ -388,10 +341,7 @@ export const useChatStore = defineStore("chat", () => {
cancelGeneration,
saveMessageAsNote,
summarizeAsNote,
fetchModels,
fetchRunningModels,
warmModel,
updateConversationModel,
fetchStatus,
startStatusPolling,
stopStatusPolling,
+1 -34
View File
@@ -1,13 +1,12 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
import { apiGet, apiPut, apiPost, apiStreamPost } from "@/api/client";
import { apiGet, apiPut } from "@/api/client";
import { useToastStore } from "@/stores/toast";
import type { AppSettings } from "@/types/settings";
export const useSettingsStore = defineStore("settings", () => {
const settings = ref<AppSettings>({});
const loading = ref(false);
const installedModels = ref<string[]>([]);
const assistantName = computed(
() => settings.value.assistant_name || "Fable"
@@ -40,44 +39,12 @@ export const useSettingsStore = defineStore("settings", () => {
}
}
async function fetchInstalledModels() {
try {
const data = await apiGet<{ models: { name: string }[] }>("/api/chat/models");
installedModels.value = data.models.map((m) => m.name);
} catch {
installedModels.value = [];
}
}
async function pullModel(
model: string,
onProgress?: (data: Record<string, unknown>) => void,
) {
await apiStreamPost("/api/chat/models/pull", { model }, (data) => {
if (onProgress) onProgress(data);
});
}
async function deleteModel(model: string) {
try {
await apiPost("/api/chat/models/delete", { model });
await fetchInstalledModels();
} catch (e) {
useToastStore().show("Failed to delete model", "error");
throw e;
}
}
return {
settings,
loading,
installedModels,
assistantName,
defaultModel,
fetchSettings,
updateSettings,
fetchInstalledModels,
pullModel,
deleteModel,
};
});