UX: queue bubbles, dashboard queuing, duplicate confirm buttons, short-note fix
- Dashboard chat: allow typing/sending during streaming (queued in store) placeholder updates to "Type to queue…" during generation - ChatView/WorkspaceView: replace queued chip with actual pending message bubbles — light grey, dashed border, "Queued" badge above content; clear button below the stack - ToolCallCard: when tool returns requires_confirmation=True, show "Similar content found" label (not "Error"), link to the similar note, and "Create Anyway" / "Don't Create" buttons that auto-send the reply - tools.py: fuzzy title match now returns requires_confirmation=True with similar_note data instead of a hard error, so numbered-series notes (Lore: X 0, Lore: X 1) can be created with one button click; semantic match responses also include similar_note for the link Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -126,7 +126,7 @@ defineExpose({ focus });
|
|||||||
<button
|
<button
|
||||||
class="btn-attach"
|
class="btn-attach"
|
||||||
@click="toggleNotePicker"
|
@click="toggleNotePicker"
|
||||||
:disabled="!store.chatReady || store.streaming"
|
:disabled="!store.chatReady"
|
||||||
title="Attach a note"
|
title="Attach a note"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -181,17 +181,17 @@ defineExpose({ focus });
|
|||||||
@input="autoResize"
|
@input="autoResize"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
!store.chatReady ? 'Chat unavailable'
|
!store.chatReady ? 'Chat unavailable'
|
||||||
: store.streaming ? 'Generating response…'
|
: store.streaming ? 'Type to queue… (Enter to queue)'
|
||||||
: 'Start a new chat… (Enter to send)'
|
: 'Start a new chat… (Enter to send)'
|
||||||
"
|
"
|
||||||
:disabled="!store.chatReady || store.streaming"
|
:disabled="!store.chatReady"
|
||||||
rows="1"
|
rows="1"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn-send"
|
class="btn-send"
|
||||||
@click="onSubmit"
|
@click="onSubmit"
|
||||||
:disabled="!messageInput.trim() || !store.chatReady || store.streaming"
|
:disabled="!messageInput.trim() || !store.chatReady"
|
||||||
>
|
>
|
||||||
↑
|
↑
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { apiPost } from "@/api/client";
|
import { apiPost } from "@/api/client";
|
||||||
|
import { useChatStore } from "@/stores/chat";
|
||||||
import type { ToolCallRecord } from "@/types/chat";
|
import type { ToolCallRecord } from "@/types/chat";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
toolCall: ToolCallRecord;
|
toolCall: ToolCallRecord;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
const appliedTags = ref<Set<string>>(new Set());
|
const appliedTags = ref<Set<string>>(new Set());
|
||||||
const applyingTag = ref<string | null>(null);
|
const applyingTag = ref<string | null>(null);
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
if (props.toolCall.status === "declined") return "Declined";
|
if (props.toolCall.status === "declined") return "Declined";
|
||||||
|
if (props.toolCall.result.requires_confirmation) return "Similar content found";
|
||||||
if (!props.toolCall.result.success) return "Error";
|
if (!props.toolCall.result.success) return "Error";
|
||||||
switch (props.toolCall.result.type) {
|
switch (props.toolCall.result.type) {
|
||||||
case "task": return "Created task";
|
case "task": return "Created task";
|
||||||
@@ -210,6 +213,14 @@ function toggle() {
|
|||||||
if (hasDetail.value) collapsed.value = !collapsed.value;
|
if (hasDetail.value) collapsed.value = !collapsed.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmDuplicate() {
|
||||||
|
chatStore.sendMessage("Yes, please create it.", null, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function denyDuplicate() {
|
||||||
|
chatStore.sendMessage("No, skip that creation.", null, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
async function applyTag(tag: string) {
|
async function applyTag(tag: string) {
|
||||||
if (!noteId.value || appliedTags.value.has(tag) || applyingTag.value) return;
|
if (!noteId.value || appliedTags.value.has(tag) || applyingTag.value) return;
|
||||||
applyingTag.value = tag;
|
applyingTag.value = tag;
|
||||||
@@ -228,7 +239,8 @@ async function applyTag(tag: string) {
|
|||||||
<div
|
<div
|
||||||
class="tool-call-card"
|
class="tool-call-card"
|
||||||
:class="{
|
:class="{
|
||||||
error: toolCall.status === 'error',
|
error: toolCall.status === 'error' && !toolCall.result.requires_confirmation,
|
||||||
|
'requires-confirm': toolCall.result.requires_confirmation,
|
||||||
declined: toolCall.status === 'declined',
|
declined: toolCall.status === 'declined',
|
||||||
running: toolCall.status === 'running',
|
running: toolCall.status === 'running',
|
||||||
collapsible: hasDetail,
|
collapsible: hasDetail,
|
||||||
@@ -244,6 +256,15 @@ async function applyTag(tag: string) {
|
|||||||
{{ toolCall.arguments.title ?? toolCall.arguments.summary ?? toolCall.arguments.query ?? toolCall.function }}
|
{{ toolCall.arguments.title ?? toolCall.arguments.summary ?? toolCall.arguments.query ?? toolCall.function }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="toolCall.result.requires_confirmation">
|
||||||
|
<router-link
|
||||||
|
v-if="toolCall.result.similar_note"
|
||||||
|
:to="`/notes/${toolCall.result.similar_note.id}`"
|
||||||
|
class="tool-link"
|
||||||
|
@click.stop
|
||||||
|
>{{ toolCall.result.similar_note.title }}</router-link>
|
||||||
|
<span v-else class="tool-error">{{ toolCall.result.error }}</span>
|
||||||
|
</template>
|
||||||
<template v-else-if="toolCall.status === 'error'">
|
<template v-else-if="toolCall.status === 'error'">
|
||||||
<span class="tool-error">{{ toolCall.result.error }}</span>
|
<span class="tool-error">{{ toolCall.result.error }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -305,6 +326,12 @@ async function applyTag(tag: string) {
|
|||||||
<span v-if="hasDetail" class="tool-chevron" :class="{ open: !collapsed }">▶</span>
|
<span v-if="hasDetail" class="tool-chevron" :class="{ open: !collapsed }">▶</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Duplicate confirmation actions -->
|
||||||
|
<div v-if="toolCall.result.requires_confirmation" class="duplicate-confirm-actions">
|
||||||
|
<button class="btn-confirm-duplicate" @click="confirmDuplicate">Create Anyway</button>
|
||||||
|
<button class="btn-deny-duplicate" @click="denyDuplicate">Don't Create</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Detail section (expandable) ─────────────────────────── -->
|
<!-- ── Detail section (expandable) ─────────────────────────── -->
|
||||||
<div v-if="hasDetail" v-show="!collapsed" class="tool-card-detail">
|
<div v-if="hasDetail" v-show="!collapsed" class="tool-card-detail">
|
||||||
<template v-if="noteContent && noteContent.tags.length">
|
<template v-if="noteContent && noteContent.tags.length">
|
||||||
@@ -577,4 +604,40 @@ async function applyTag(tag: string) {
|
|||||||
}
|
}
|
||||||
.tag-pill:disabled:not(.applied) { opacity: 0.6; cursor: wait; }
|
.tag-pill:disabled:not(.applied) { opacity: 0.6; cursor: wait; }
|
||||||
.tag-check { font-size: 0.65rem; }
|
.tag-check { font-size: 0.65rem; }
|
||||||
|
|
||||||
|
/* Duplicate confirmation */
|
||||||
|
.tool-call-card.requires-confirm {
|
||||||
|
border-color: color-mix(in srgb, var(--color-warning, #f59e0b) 60%, transparent);
|
||||||
|
}
|
||||||
|
.duplicate-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.btn-confirm-duplicate {
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm, 6px);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.btn-confirm-duplicate:hover { opacity: 0.9; }
|
||||||
|
.btn-deny-duplicate {
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm, 6px);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.btn-deny-duplicate:hover {
|
||||||
|
color: var(--color-danger, #e74c3c);
|
||||||
|
border-color: var(--color-danger, #e74c3c);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
return id ? (convQueues.value[id]?.length ?? 0) : 0;
|
return id ? (convQueues.value[id]?.length ?? 0) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queuedMessages = computed(() => {
|
||||||
|
const id = currentConversation.value?.id;
|
||||||
|
return id ? (convQueues.value[id] ?? []) : [];
|
||||||
|
});
|
||||||
|
|
||||||
function clearQueue() {
|
function clearQueue() {
|
||||||
const id = currentConversation.value?.id;
|
const id = currentConversation.value?.id;
|
||||||
if (id) convQueues.value[id] = [];
|
if (id) convQueues.value[id] = [];
|
||||||
@@ -556,6 +561,7 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
chatReady,
|
chatReady,
|
||||||
isStreamingConv,
|
isStreamingConv,
|
||||||
queuedCount,
|
queuedCount,
|
||||||
|
queuedMessages,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
fetchConversations,
|
fetchConversations,
|
||||||
createConversation,
|
createConversation,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface GenerationTiming {
|
|||||||
export interface ToolCallRecord {
|
export interface ToolCallRecord {
|
||||||
function: string;
|
function: string;
|
||||||
arguments: Record<string, unknown>;
|
arguments: Record<string, unknown>;
|
||||||
result: { success: boolean; type?: string; data?: Record<string, unknown>; error?: string; suggested_tags?: string[] };
|
result: { success: boolean; type?: string; data?: Record<string, unknown>; error?: string; suggested_tags?: string[]; requires_confirmation?: boolean; similar_note?: { id: number; title: string } };
|
||||||
status: "running" | "success" | "error" | "declined";
|
status: "running" | "success" | "error" | "declined";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -632,12 +632,24 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Queued message indicator -->
|
<!-- Queued messages — shown as pending bubbles -->
|
||||||
<div v-if="store.queuedCount" class="queued-indicator">
|
<template v-if="store.queuedMessages.length">
|
||||||
<span class="queued-icon">⏳</span>
|
<div
|
||||||
{{ store.queuedCount }} message{{ store.queuedCount > 1 ? 's' : '' }} queued
|
v-for="(q, i) in store.queuedMessages"
|
||||||
<button class="queued-cancel" @click="store.clearQueue()" aria-label="Cancel queued messages">×</button>
|
:key="`queued-${i}`"
|
||||||
</div>
|
class="chat-message role-user queued-message"
|
||||||
|
>
|
||||||
|
<div class="message-bubble queued-bubble">
|
||||||
|
<div class="queued-badge">Queued</div>
|
||||||
|
<div class="message-content">{{ q.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queued-clear-row">
|
||||||
|
<button class="queued-clear-btn" @click="store.clearQueue()" aria-label="Cancel queued messages">
|
||||||
|
Cancel {{ store.queuedMessages.length }} queued
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="!store.currentConversation.messages.length && !store.streaming"
|
v-if="!store.currentConversation.messages.length && !store.streaming"
|
||||||
@@ -1610,29 +1622,36 @@ details[open] .thinking-summary::before {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.queued-indicator {
|
.queued-bubble {
|
||||||
display: flex;
|
background: var(--color-bg-secondary) !important;
|
||||||
align-items: center;
|
border: 1px dashed var(--color-border) !important;
|
||||||
gap: 0.4rem;
|
opacity: 0.7;
|
||||||
font-size: 0.8rem;
|
}
|
||||||
|
.queued-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
padding: 0.3rem 1rem;
|
margin-bottom: 0.25rem;
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
}
|
||||||
.queued-icon {
|
.queued-clear-row {
|
||||||
font-size: 0.75rem;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
.queued-cancel {
|
.queued-clear-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm, 6px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 1rem;
|
font-size: 0.78rem;
|
||||||
line-height: 1;
|
padding: 0.2rem 0.6rem;
|
||||||
padding: 0 0.2rem;
|
font-family: inherit;
|
||||||
margin-left: 0.1rem;
|
|
||||||
}
|
}
|
||||||
.queued-cancel:hover {
|
.queued-clear-btn:hover {
|
||||||
color: var(--color-text);
|
color: var(--color-danger, #e74c3c);
|
||||||
|
border-color: var(--color-danger, #e74c3c);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -318,12 +318,24 @@ onUnmounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Queued message indicator -->
|
<!-- Queued messages — shown as pending bubbles -->
|
||||||
<div v-if="chatStore.queuedCount" class="queued-indicator">
|
<template v-if="chatStore.queuedMessages.length">
|
||||||
<span>⏳</span>
|
<div
|
||||||
{{ chatStore.queuedCount }} message{{ chatStore.queuedCount > 1 ? 's' : '' }} queued
|
v-for="(q, i) in chatStore.queuedMessages"
|
||||||
<button class="queued-cancel" @click="chatStore.clearQueue()" aria-label="Cancel queued messages">×</button>
|
:key="`queued-${i}`"
|
||||||
</div>
|
class="ws-message role-user queued-message"
|
||||||
|
>
|
||||||
|
<div class="message-bubble queued-bubble">
|
||||||
|
<div class="queued-badge">Queued</div>
|
||||||
|
<div class="message-content">{{ q.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queued-clear-row">
|
||||||
|
<button class="queued-clear-btn" @click="chatStore.clearQueue()" aria-label="Cancel queued messages">
|
||||||
|
Cancel {{ chatStore.queuedMessages.length }} queued
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="chatStore.currentConversation && !chatStore.currentConversation.messages.length && !chatStore.streaming"
|
v-if="chatStore.currentConversation && !chatStore.currentConversation.messages.length && !chatStore.streaming"
|
||||||
@@ -631,25 +643,36 @@ details[open] .thinking-summary::before {
|
|||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.queued-indicator {
|
.queued-bubble {
|
||||||
display: flex;
|
background: var(--color-bg-secondary) !important;
|
||||||
align-items: center;
|
border: 1px dashed var(--color-border) !important;
|
||||||
gap: 0.4rem;
|
opacity: 0.7;
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
padding: 0.3rem 0.75rem;
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
}
|
||||||
.queued-cancel {
|
.queued-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.queued-clear-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.queued-clear-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm, 6px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 1rem;
|
font-size: 0.78rem;
|
||||||
line-height: 1;
|
padding: 0.2rem 0.6rem;
|
||||||
padding: 0 0.2rem;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.queued-cancel:hover {
|
.queued-clear-btn:hover {
|
||||||
color: var(--color-text);
|
color: var(--color-danger, #e74c3c);
|
||||||
|
border-color: var(--color-danger, #e74c3c);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
|||||||
near, ratio = _fuzzy_title_match(task_title, candidates)
|
near, ratio = _fuzzy_title_match(task_title, candidates)
|
||||||
if near is not None:
|
if near is not None:
|
||||||
item_type = "task" if near.status is not None else "note"
|
item_type = "task" if near.status is not None else "note"
|
||||||
return {"success": False, "error": f"A {item_type} with a very similar title '{near.title}' already exists (id: {near.id}, similarity: {ratio:.0%}). Use update_note to modify it instead of creating a duplicate."}
|
return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A {item_type} with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."}
|
||||||
|
|
||||||
if not arguments.get("confirmed") and len(task_body.strip()) >= 80:
|
if not arguments.get("confirmed") and len(task_body.strip()) >= 80:
|
||||||
from fabledassistant.services.embeddings import semantic_search_notes as _ssn
|
from fabledassistant.services.embeddings import semantic_search_notes as _ssn
|
||||||
@@ -832,7 +832,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
|||||||
if sem_hits:
|
if sem_hits:
|
||||||
best_score, best_note = sem_hits[0]
|
best_score, best_note = sem_hits[0]
|
||||||
item_type = "task" if best_note.status is not None else "note"
|
item_type = "task" if best_note.status is not None else "note"
|
||||||
return {"success": False, "requires_confirmation": True, "error": f"A {item_type} with very similar content exists: '{best_note.title}' (id: {best_note.id}, semantic similarity: {best_score:.0%}). Ask the user: 'An existing {item_type} covers very similar ground. Shall I still create a separate task titled \"{task_title}\"?' Then retry with confirmed=true if they agree."}
|
return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A {item_type} with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."}
|
||||||
|
|
||||||
note = await create_note(
|
note = await create_note(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -901,7 +901,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
|||||||
near, ratio = _fuzzy_title_match(note_title, candidates)
|
near, ratio = _fuzzy_title_match(note_title, candidates)
|
||||||
if near is not None:
|
if near is not None:
|
||||||
item_type = "task" if near.status is not None else "note"
|
item_type = "task" if near.status is not None else "note"
|
||||||
return {"success": False, "error": f"A {item_type} with a very similar title '{near.title}' already exists (id: {near.id}, similarity: {ratio:.0%}). Use update_note to modify it instead of creating a duplicate."}
|
return {"success": False, "requires_confirmation": True, "similar_note": {"id": near.id, "title": near.title}, "error": f"A {item_type} with a very similar title '{near.title}' already exists (similarity: {ratio:.0%}). Ask the user to confirm before creating a separate entry."}
|
||||||
|
|
||||||
if not arguments.get("confirmed") and len(note_body.strip()) >= 80:
|
if not arguments.get("confirmed") and len(note_body.strip()) >= 80:
|
||||||
from fabledassistant.services.embeddings import semantic_search_notes as _ssn
|
from fabledassistant.services.embeddings import semantic_search_notes as _ssn
|
||||||
@@ -910,7 +910,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
|||||||
if sem_hits:
|
if sem_hits:
|
||||||
best_score, best_note = sem_hits[0]
|
best_score, best_note = sem_hits[0]
|
||||||
item_type = "task" if best_note.status is not None else "note"
|
item_type = "task" if best_note.status is not None else "note"
|
||||||
return {"success": False, "requires_confirmation": True, "error": f"A {item_type} with very similar content exists: '{best_note.title}' (id: {best_note.id}, semantic similarity: {best_score:.0%}). Ask the user: 'An existing {item_type} covers very similar ground. Shall I still create a separate note titled \"{note_title}\"?' Then retry with confirmed=true if they agree."}
|
return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A {item_type} with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."}
|
||||||
|
|
||||||
note = await create_note(
|
note = await create_note(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user