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
|
||||
class="btn-attach"
|
||||
@click="toggleNotePicker"
|
||||
:disabled="!store.chatReady || store.streaming"
|
||||
:disabled="!store.chatReady"
|
||||
title="Attach a note"
|
||||
>
|
||||
<svg
|
||||
@@ -181,17 +181,17 @@ defineExpose({ focus });
|
||||
@input="autoResize"
|
||||
:placeholder="
|
||||
!store.chatReady ? 'Chat unavailable'
|
||||
: store.streaming ? 'Generating response…'
|
||||
: store.streaming ? 'Type to queue… (Enter to queue)'
|
||||
: 'Start a new chat… (Enter to send)'
|
||||
"
|
||||
:disabled="!store.chatReady || store.streaming"
|
||||
:disabled="!store.chatReady"
|
||||
rows="1"
|
||||
></textarea>
|
||||
|
||||
<button
|
||||
class="btn-send"
|
||||
@click="onSubmit"
|
||||
:disabled="!messageInput.trim() || !store.chatReady || store.streaming"
|
||||
:disabled="!messageInput.trim() || !store.chatReady"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { apiPost } from "@/api/client";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
import type { ToolCallRecord } from "@/types/chat";
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: ToolCallRecord;
|
||||
}>();
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const appliedTags = ref<Set<string>>(new Set());
|
||||
const applyingTag = ref<string | null>(null);
|
||||
|
||||
const label = computed(() => {
|
||||
if (props.toolCall.status === "declined") return "Declined";
|
||||
if (props.toolCall.result.requires_confirmation) return "Similar content found";
|
||||
if (!props.toolCall.result.success) return "Error";
|
||||
switch (props.toolCall.result.type) {
|
||||
case "task": return "Created task";
|
||||
@@ -210,6 +213,14 @@ function toggle() {
|
||||
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) {
|
||||
if (!noteId.value || appliedTags.value.has(tag) || applyingTag.value) return;
|
||||
applyingTag.value = tag;
|
||||
@@ -228,7 +239,8 @@ async function applyTag(tag: string) {
|
||||
<div
|
||||
class="tool-call-card"
|
||||
:class="{
|
||||
error: toolCall.status === 'error',
|
||||
error: toolCall.status === 'error' && !toolCall.result.requires_confirmation,
|
||||
'requires-confirm': toolCall.result.requires_confirmation,
|
||||
declined: toolCall.status === 'declined',
|
||||
running: toolCall.status === 'running',
|
||||
collapsible: hasDetail,
|
||||
@@ -244,6 +256,15 @@ async function applyTag(tag: string) {
|
||||
{{ toolCall.arguments.title ?? toolCall.arguments.summary ?? toolCall.arguments.query ?? toolCall.function }}
|
||||
</span>
|
||||
</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'">
|
||||
<span class="tool-error">{{ toolCall.result.error }}</span>
|
||||
</template>
|
||||
@@ -305,6 +326,12 @@ async function applyTag(tag: string) {
|
||||
<span v-if="hasDetail" class="tool-chevron" :class="{ open: !collapsed }">▶</span>
|
||||
</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) ─────────────────────────── -->
|
||||
<div v-if="hasDetail" v-show="!collapsed" class="tool-card-detail">
|
||||
<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-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>
|
||||
|
||||
Reference in New Issue
Block a user