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:
@@ -5,6 +5,7 @@ import { useNotesStore } from "@/stores/notes";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { renderMarkdown } from "@/utils/markdown";
|
||||
import { useAssist } from "@/composables/useAssist";
|
||||
import { apiPost } from "@/api/client";
|
||||
import type { Editor } from "@tiptap/vue-3";
|
||||
import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
|
||||
import TiptapEditor from "@/components/TiptapEditor.vue";
|
||||
@@ -55,6 +56,41 @@ function truncateTarget(text: string, max = 60): string {
|
||||
return first.length > max ? first.slice(0, max) + "..." : first;
|
||||
}
|
||||
|
||||
// Tag suggestions
|
||||
const suggestedTags = ref<string[]>([]);
|
||||
const appliedTags = ref<Set<string>>(new Set());
|
||||
const suggestingTags = ref(false);
|
||||
|
||||
async function fetchTagSuggestions() {
|
||||
if (suggestingTags.value) return;
|
||||
suggestingTags.value = true;
|
||||
suggestedTags.value = [];
|
||||
appliedTags.value = new Set();
|
||||
try {
|
||||
const res = await apiPost<{ suggested_tags: string[] }>("/api/notes/suggest-tags", {
|
||||
title: title.value,
|
||||
body: body.value,
|
||||
});
|
||||
suggestedTags.value = res.suggested_tags;
|
||||
} catch {
|
||||
toast.show("Failed to get tag suggestions", "error");
|
||||
} finally {
|
||||
suggestingTags.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTagSuggestion(tag: string) {
|
||||
if (appliedTags.value.has(tag)) return;
|
||||
body.value = body.value.trimEnd() + `\n#${tag}`;
|
||||
appliedTags.value.add(tag);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function dismissTagSuggestions() {
|
||||
suggestedTags.value = [];
|
||||
appliedTags.value = new Set();
|
||||
}
|
||||
|
||||
// Track saved state for dirty detection
|
||||
let savedTitle = "";
|
||||
let savedBody = "";
|
||||
@@ -178,6 +214,25 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
@input="markDirty"
|
||||
/>
|
||||
|
||||
<div class="tag-suggest-row">
|
||||
<button class="btn-suggest-tags" @click="fetchTagSuggestions" :disabled="suggestingTags">
|
||||
{{ suggestingTags ? "Suggesting..." : "Suggest tags" }}
|
||||
</button>
|
||||
<template v-if="suggestedTags.length > 0">
|
||||
<button
|
||||
v-for="tag in suggestedTags"
|
||||
:key="tag"
|
||||
:class="['tag-pill', { applied: appliedTags.has(tag) }]"
|
||||
:disabled="appliedTags.has(tag)"
|
||||
@click="applyTagSuggestion(tag)"
|
||||
>
|
||||
#{{ tag }}
|
||||
<span v-if="appliedTags.has(tag)" class="tag-check">✓</span>
|
||||
</button>
|
||||
<button class="btn-dismiss-tags" @click="dismissTagSuggestions">×</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="editor-tabs">
|
||||
<button
|
||||
:class="['tab', { active: !showPreview }]"
|
||||
@@ -385,6 +440,69 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
|
||||
/* ── Tag suggestions ── */
|
||||
.tag-suggest-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.btn-suggest-tags {
|
||||
padding: 0.3rem 0.7rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.btn-suggest-tags:hover:not(:disabled) {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.btn-suggest-tags:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
.tag-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.tag-pill:hover:not(:disabled) {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.tag-pill.applied {
|
||||
background: var(--color-success, #2ecc71);
|
||||
border-color: var(--color-success, #2ecc71);
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
.tag-check {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.btn-dismiss-tags {
|
||||
padding: 0.1rem 0.4rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.btn-dismiss-tags:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ── Assist panel ── */
|
||||
.assist-panel {
|
||||
flex: 0 0 33.33%;
|
||||
|
||||
Reference in New Issue
Block a user