Collapse tool call cards by default; expand on click
- Completed (success) tool calls render collapsed: label + inline summary + ▶ chevron - Clicking the header expands the detail section (result list, links, tags) - Error cards start expanded so failures are immediately visible - Running (in-progress) cards start expanded for live progress visibility - Auto-collapses when a running call completes during streaming - Suggested-tag pills remain accessible in both collapsed (via separate row) and expanded states Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { apiPost } from "@/api/client";
|
import { apiPost } from "@/api/client";
|
||||||
import type { ToolCallRecord } from "@/types/chat";
|
import type { ToolCallRecord } from "@/types/chat";
|
||||||
|
|
||||||
@@ -14,54 +14,30 @@ const label = computed(() => {
|
|||||||
if (props.toolCall.status === "declined") return "Declined";
|
if (props.toolCall.status === "declined") return "Declined";
|
||||||
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":
|
case "task": return "Created task";
|
||||||
return "Created task";
|
case "note": return "Created note";
|
||||||
case "note":
|
case "note_updated": return "Updated note";
|
||||||
return "Created note";
|
case "note_deleted": return "Deleted note";
|
||||||
case "note_updated":
|
case "task_deleted": return "Deleted task";
|
||||||
return "Updated note";
|
case "note_content": return "Note";
|
||||||
case "note_deleted":
|
case "notes_list": return "Notes";
|
||||||
return "Deleted note";
|
case "tasks": return "Tasks";
|
||||||
case "task_deleted":
|
case "todo_updated": return "Updated todo";
|
||||||
return "Deleted task";
|
case "search": return "Searched notes";
|
||||||
case "note_content":
|
case "event": return "Created event";
|
||||||
return "Note";
|
case "events": return "Found events";
|
||||||
case "notes_list":
|
case "event_updated": return "Updated event";
|
||||||
return "Notes";
|
case "event_deleted": return "Deleted event";
|
||||||
case "tasks":
|
case "calendars": return "Calendars";
|
||||||
return "Tasks";
|
case "todo": return "Created todo";
|
||||||
case "todo_updated":
|
case "todos": return "Calendar todos";
|
||||||
return "Updated todo";
|
case "todo_completed": return "Completed todo";
|
||||||
case "search":
|
case "todo_deleted": return "Deleted todo";
|
||||||
return "Searched notes";
|
case "web_search": return "Web search";
|
||||||
case "event":
|
case "research_pending": return "Research started";
|
||||||
return "Created event";
|
case "research_note": return "Research note";
|
||||||
case "events":
|
case "image_search": return "Image search";
|
||||||
return "Found events";
|
default: return "Tool call";
|
||||||
case "event_updated":
|
|
||||||
return "Updated event";
|
|
||||||
case "event_deleted":
|
|
||||||
return "Deleted event";
|
|
||||||
case "calendars":
|
|
||||||
return "Calendars";
|
|
||||||
case "todo":
|
|
||||||
return "Created todo";
|
|
||||||
case "todos":
|
|
||||||
return "Calendar todos";
|
|
||||||
case "todo_completed":
|
|
||||||
return "Completed todo";
|
|
||||||
case "todo_deleted":
|
|
||||||
return "Deleted todo";
|
|
||||||
case "web_search":
|
|
||||||
return "Web search";
|
|
||||||
case "research_pending":
|
|
||||||
return "Research started";
|
|
||||||
case "research_note":
|
|
||||||
return "Research note";
|
|
||||||
case "image_search":
|
|
||||||
return "Image search";
|
|
||||||
default:
|
|
||||||
return "Tool call";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,9 +60,7 @@ const noteId = computed(() => {
|
|||||||
return data.id;
|
return data.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const suggestedTags = computed(() => {
|
const suggestedTags = computed(() => props.toolCall.result.suggested_tags ?? []);
|
||||||
return props.toolCall.result.suggested_tags ?? [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventData = computed(() => {
|
const eventData = computed(() => {
|
||||||
const data = props.toolCall.result.data;
|
const data = props.toolCall.result.data;
|
||||||
@@ -123,8 +97,7 @@ const todoData = computed(() => {
|
|||||||
const todoList = computed(() => {
|
const todoList = computed(() => {
|
||||||
const data = props.toolCall.result.data;
|
const data = props.toolCall.result.data;
|
||||||
if (!data || props.toolCall.result.type !== "todos") return null;
|
if (!data || props.toolCall.result.type !== "todos") return null;
|
||||||
const todos = data.todos as Array<{ summary: string; due?: string; status?: string }> | undefined;
|
return (data.todos as Array<{ summary: string; due?: string; status?: string }> | undefined) ?? [];
|
||||||
return todos ?? [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const todoCount = computed(() => {
|
const todoCount = computed(() => {
|
||||||
@@ -136,8 +109,7 @@ const todoCount = computed(() => {
|
|||||||
const eventList = computed(() => {
|
const eventList = computed(() => {
|
||||||
const data = props.toolCall.result.data;
|
const data = props.toolCall.result.data;
|
||||||
if (!data || props.toolCall.result.type !== "events") return null;
|
if (!data || props.toolCall.result.type !== "events") return null;
|
||||||
const events = data.events as Array<{ title: string; start: string; end: string; location?: string }> | undefined;
|
return (data.events as Array<{ title: string; start: string; end: string; location?: string }> | undefined) ?? [];
|
||||||
return events ?? [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventCount = computed(() => {
|
const eventCount = computed(() => {
|
||||||
@@ -149,24 +121,17 @@ const eventCount = computed(() => {
|
|||||||
function formatEventTime(iso: string): string {
|
function formatEventTime(iso: string): string {
|
||||||
if (!iso) return "";
|
if (!iso) return "";
|
||||||
try {
|
try {
|
||||||
const d = new Date(iso);
|
return new Date(iso).toLocaleString(undefined, {
|
||||||
return d.toLocaleString(undefined, {
|
weekday: "short", month: "short", day: "numeric",
|
||||||
weekday: "short",
|
hour: "numeric", minute: "2-digit",
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch { return iso; }
|
||||||
return iso;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskList = computed(() => {
|
const taskList = computed(() => {
|
||||||
const data = props.toolCall.result.data;
|
const data = props.toolCall.result.data;
|
||||||
if (!data || props.toolCall.result.type !== "tasks") return null;
|
if (!data || props.toolCall.result.type !== "tasks") return null;
|
||||||
const results = data.results as Array<{ id: number; title: string; status: string; priority: string; due_date?: string }> | undefined;
|
return (data.results as Array<{ id: number; title: string; status: string; priority: string; due_date?: string }> | undefined) ?? [];
|
||||||
return results ?? [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const taskCount = computed(() => {
|
const taskCount = computed(() => {
|
||||||
@@ -204,8 +169,7 @@ const noteContent = computed(() => {
|
|||||||
const noteList = computed(() => {
|
const noteList = computed(() => {
|
||||||
const data = props.toolCall.result.data;
|
const data = props.toolCall.result.data;
|
||||||
if (!data || props.toolCall.result.type !== "notes_list") return null;
|
if (!data || props.toolCall.result.type !== "notes_list") return null;
|
||||||
const results = data.results as Array<{ id: number; title: string; tags: string[]; preview: string }> | undefined;
|
return (data.results as Array<{ id: number; title: string; tags: string[]; preview: string }> | undefined) ?? [];
|
||||||
return results ?? [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const noteListCount = computed(() => {
|
const noteListCount = computed(() => {
|
||||||
@@ -214,6 +178,38 @@ const noteListCount = computed(() => {
|
|||||||
return (data.total as number) ?? 0;
|
return (data.total as number) ?? 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Collapse logic ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Cards with rich expandable content
|
||||||
|
const hasDetail = computed(() => {
|
||||||
|
if (props.toolCall.status === "running") return false;
|
||||||
|
return (
|
||||||
|
(noteContent.value !== null && noteContent.value.tags.length > 0) ||
|
||||||
|
(noteList.value !== null && noteList.value.length > 0) ||
|
||||||
|
searchResults.value !== null ||
|
||||||
|
(taskList.value !== null && taskList.value.length > 0) ||
|
||||||
|
(eventList.value !== null && eventList.value.length > 0) ||
|
||||||
|
(todoList.value !== null && todoList.value.length > 0) ||
|
||||||
|
webSearch.value !== null ||
|
||||||
|
(calendarList.value !== null && calendarList.value.length > 0) ||
|
||||||
|
suggestedTags.value.length > 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collapsed by default for completed calls; errors and running stay open
|
||||||
|
const collapsed = ref(
|
||||||
|
props.toolCall.status === "success" || props.toolCall.status === "declined"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-collapse when a running tool call completes
|
||||||
|
watch(() => props.toolCall.status, (s) => {
|
||||||
|
if (s === "success") collapsed.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (hasDetail.value) collapsed.value = !collapsed.value;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -229,127 +225,185 @@ async function applyTag(tag: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tool-call-card" :class="{ error: toolCall.status === 'error', declined: toolCall.status === 'declined' }">
|
<div
|
||||||
<span class="tool-label">{{ label }}</span>
|
class="tool-call-card"
|
||||||
<template v-if="toolCall.status === 'declined'">
|
:class="{
|
||||||
<span class="tool-declined-name">{{ toolCall.arguments.title ?? toolCall.arguments.summary ?? toolCall.arguments.query ?? toolCall.function }}</span>
|
error: toolCall.status === 'error',
|
||||||
</template>
|
declined: toolCall.status === 'declined',
|
||||||
<template v-else-if="toolCall.status === 'error'">
|
running: toolCall.status === 'running',
|
||||||
<span class="tool-error">{{ toolCall.result.error }}</span>
|
collapsible: hasDetail,
|
||||||
</template>
|
}"
|
||||||
<template v-else-if="deletedNote">
|
>
|
||||||
<span class="tool-deleted">{{ deletedNote.title || "Untitled" }}</span>
|
<!-- ── Header row (always visible) ─────────────────────────── -->
|
||||||
</template>
|
<div class="tool-card-header" :class="{ clickable: hasDetail }" @click="toggle">
|
||||||
<template v-else-if="noteContent">
|
<span class="tool-label">{{ label }}</span>
|
||||||
<router-link :to="`/notes/${noteContent.id}`" class="tool-link">{{ noteContent.title || "Untitled" }}</router-link>
|
|
||||||
<span v-if="noteContent.tags.length" class="tool-note-tags">
|
|
||||||
<span v-for="tag in noteContent.tags" :key="tag" class="tool-note-tag">#{{ tag }}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="noteList !== null">
|
|
||||||
<span class="tool-search-info">{{ noteListCount }} found</span>
|
|
||||||
<div v-if="noteList.length > 0" class="tool-search-results">
|
|
||||||
<router-link
|
|
||||||
v-for="n in noteList.slice(0, 8)"
|
|
||||||
:key="n.id"
|
|
||||||
:to="`/notes/${n.id}`"
|
|
||||||
class="tool-search-item"
|
|
||||||
>
|
|
||||||
{{ n.title || "Untitled" }}
|
|
||||||
</router-link>
|
|
||||||
<span v-if="noteList.length > 8" class="tool-event-more">+{{ noteList.length - 8 }} more</span>
|
|
||||||
</div>
|
|
||||||
<span v-else class="tool-search-info">No notes found</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="searchResults">
|
|
||||||
<span class="tool-search-info">{{ (toolCall.result.data?.count as number) ?? (toolCall.result.data?.total as number) ?? 0 }} found</span>
|
|
||||||
<div class="tool-search-results">
|
|
||||||
<router-link
|
|
||||||
v-for="r in searchResults"
|
|
||||||
:key="r.id"
|
|
||||||
:to="`/notes/${r.id}`"
|
|
||||||
class="tool-search-item"
|
|
||||||
>
|
|
||||||
{{ r.title || "Untitled" }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="eventData">
|
|
||||||
<span class="tool-event-title">{{ eventData.title }}</span>
|
|
||||||
<span class="tool-event-time">{{ formatEventTime(eventData.start) }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="updatedEvent">
|
|
||||||
<span class="tool-event-title">{{ updatedEvent.title }}</span>
|
|
||||||
<span class="tool-event-time">{{ formatEventTime(updatedEvent.start) }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="deletedEvent">
|
|
||||||
<span class="tool-deleted">{{ deletedEvent.title }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="calendarList !== null">
|
|
||||||
<div class="tool-calendar-list">
|
|
||||||
<span v-for="name in calendarList" :key="name" class="tool-calendar-name">{{ name }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="todoData">
|
|
||||||
<span :class="{ 'tool-deleted': toolCall.result.type === 'todo_deleted', 'tool-completed': toolCall.result.type === 'todo_completed' }">
|
|
||||||
{{ todoData.summary }}
|
|
||||||
</span>
|
|
||||||
<span v-if="todoData.due" class="tool-event-time">{{ formatEventTime(todoData.due) }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="todoList !== null">
|
|
||||||
<span class="tool-search-info">{{ todoCount }} found</span>
|
|
||||||
<div v-if="todoList.length > 0" class="tool-event-list">
|
|
||||||
<div v-for="(t, i) in todoList.slice(0, 5)" :key="i" class="tool-event-item">
|
|
||||||
<span class="tool-event-item-title">{{ t.summary }}</span>
|
|
||||||
<span v-if="t.due" class="tool-event-item-time">{{ formatEventTime(t.due) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="todoList.length > 5" class="tool-event-more">
|
|
||||||
+{{ todoList.length - 5 }} more
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="taskList !== null">
|
|
||||||
<span class="tool-search-info">{{ taskCount }} found</span>
|
|
||||||
<div v-if="taskList.length > 0" class="tool-event-list">
|
|
||||||
<div v-for="(t, i) in taskList.slice(0, 5)" :key="i" class="tool-event-item">
|
|
||||||
<router-link :to="`/tasks/${t.id}`" class="tool-event-item-title">{{ t.title }}</router-link>
|
|
||||||
<span v-if="t.due_date" class="tool-event-item-time">{{ t.due_date }}</span>
|
|
||||||
<span v-if="t.priority && t.priority !== 'none'" class="tool-task-priority" :class="`priority-${t.priority}`">{{ t.priority }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="taskList.length > 5" class="tool-event-more">+{{ taskList.length - 5 }} more</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="eventList !== null">
|
|
||||||
<span class="tool-search-info">{{ eventCount }} found</span>
|
|
||||||
<div v-if="eventList.length > 0" class="tool-event-list">
|
|
||||||
<div v-for="(ev, i) in eventList.slice(0, 5)" :key="i" class="tool-event-item">
|
|
||||||
<span class="tool-event-item-title">{{ ev.title }}</span>
|
|
||||||
<span class="tool-event-item-time">{{ formatEventTime(ev.start) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="eventList.length > 5" class="tool-event-more">
|
|
||||||
+{{ eventList.length - 5 }} more
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="webSearch">
|
|
||||||
<span class="tool-search-info">{{ webSearch.count }} result{{ webSearch.count !== 1 ? "s" : "" }}</span>
|
|
||||||
<div class="tool-search-results web-search-results">
|
|
||||||
<a
|
|
||||||
v-for="(r, i) in webSearch.results"
|
|
||||||
:key="i"
|
|
||||||
:href="r.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="tool-search-item"
|
|
||||||
>{{ r.title || r.url }}</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="linkTo">
|
|
||||||
<router-link :to="linkTo" class="tool-link">{{ title }}</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Suggested tags pills -->
|
<!-- Inline summary content -->
|
||||||
<div v-if="suggestedTags.length > 0" class="tag-suggestions">
|
<template v-if="toolCall.status === 'declined'">
|
||||||
|
<span class="tool-declined-name">
|
||||||
|
{{ toolCall.arguments.title ?? toolCall.arguments.summary ?? toolCall.arguments.query ?? toolCall.function }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="toolCall.status === 'error'">
|
||||||
|
<span class="tool-error">{{ toolCall.result.error }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="deletedNote">
|
||||||
|
<span class="tool-deleted">{{ deletedNote.title || "Untitled" }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="noteContent">
|
||||||
|
<router-link :to="`/notes/${noteContent.id}`" class="tool-link" @click.stop>
|
||||||
|
{{ noteContent.title || "Untitled" }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="noteList !== null">
|
||||||
|
<span class="tool-summary-count">{{ noteListCount }} found</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="searchResults">
|
||||||
|
<span class="tool-summary-count">
|
||||||
|
{{ (toolCall.result.data?.count as number) ?? (toolCall.result.data?.total as number) ?? 0 }} found
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="eventData">
|
||||||
|
<span class="tool-event-title">{{ eventData.title }}</span>
|
||||||
|
<span class="tool-event-time">{{ formatEventTime(eventData.start) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="updatedEvent">
|
||||||
|
<span class="tool-event-title">{{ updatedEvent.title }}</span>
|
||||||
|
<span class="tool-event-time">{{ formatEventTime(updatedEvent.start) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="deletedEvent">
|
||||||
|
<span class="tool-deleted">{{ deletedEvent.title }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="calendarList !== null">
|
||||||
|
<span class="tool-summary-count">{{ calendarList.length }} calendar{{ calendarList.length !== 1 ? "s" : "" }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="todoData">
|
||||||
|
<span :class="{ 'tool-deleted': toolCall.result.type === 'todo_deleted', 'tool-completed': toolCall.result.type === 'todo_completed' }">
|
||||||
|
{{ todoData.summary }}
|
||||||
|
</span>
|
||||||
|
<span v-if="todoData.due" class="tool-event-time">{{ formatEventTime(todoData.due) }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="todoList !== null">
|
||||||
|
<span class="tool-summary-count">{{ todoCount }} found</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="taskList !== null">
|
||||||
|
<span class="tool-summary-count">{{ taskCount }} found</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="eventList !== null">
|
||||||
|
<span class="tool-summary-count">{{ eventCount }} found</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="webSearch">
|
||||||
|
<span class="tool-summary-count">
|
||||||
|
{{ webSearch.count }} result{{ webSearch.count !== 1 ? "s" : "" }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="linkTo">
|
||||||
|
<router-link :to="linkTo" class="tool-link" @click.stop>{{ title }}</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Chevron toggle -->
|
||||||
|
<span v-if="hasDetail" class="tool-chevron" :class="{ open: !collapsed }">▶</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Detail section (expandable) ─────────────────────────── -->
|
||||||
|
<div v-if="hasDetail" v-show="!collapsed" class="tool-card-detail">
|
||||||
|
<template v-if="noteContent && noteContent.tags.length">
|
||||||
|
<span class="tool-note-tags">
|
||||||
|
<span v-for="tag in noteContent.tags" :key="tag" class="tool-note-tag">#{{ tag }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="noteList !== null && noteList.length > 0">
|
||||||
|
<div class="tool-search-results">
|
||||||
|
<router-link
|
||||||
|
v-for="n in noteList.slice(0, 8)"
|
||||||
|
:key="n.id"
|
||||||
|
:to="`/notes/${n.id}`"
|
||||||
|
class="tool-search-item"
|
||||||
|
>{{ n.title || "Untitled" }}</router-link>
|
||||||
|
<span v-if="noteList.length > 8" class="tool-event-more">+{{ noteList.length - 8 }} more</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="searchResults">
|
||||||
|
<div class="tool-search-results">
|
||||||
|
<router-link
|
||||||
|
v-for="r in searchResults"
|
||||||
|
:key="r.id"
|
||||||
|
:to="`/notes/${r.id}`"
|
||||||
|
class="tool-search-item"
|
||||||
|
>{{ r.title || "Untitled" }}</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="todoList !== null && todoList.length > 0">
|
||||||
|
<div class="tool-event-list">
|
||||||
|
<div v-for="(t, i) in todoList.slice(0, 5)" :key="i" class="tool-event-item">
|
||||||
|
<span class="tool-event-item-title">{{ t.summary }}</span>
|
||||||
|
<span v-if="t.due" class="tool-event-item-time">{{ formatEventTime(t.due) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="todoList.length > 5" class="tool-event-more">+{{ todoList.length - 5 }} more</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="taskList !== null && taskList.length > 0">
|
||||||
|
<div class="tool-event-list">
|
||||||
|
<div v-for="(t, i) in taskList.slice(0, 5)" :key="i" class="tool-event-item">
|
||||||
|
<router-link :to="`/tasks/${t.id}`" class="tool-event-item-title">{{ t.title }}</router-link>
|
||||||
|
<span v-if="t.due_date" class="tool-event-item-time">{{ t.due_date }}</span>
|
||||||
|
<span v-if="t.priority && t.priority !== 'none'" class="tool-task-priority" :class="`priority-${t.priority}`">{{ t.priority }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="taskList.length > 5" class="tool-event-more">+{{ taskList.length - 5 }} more</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="eventList !== null && eventList.length > 0">
|
||||||
|
<div class="tool-event-list">
|
||||||
|
<div v-for="(ev, i) in eventList.slice(0, 5)" :key="i" class="tool-event-item">
|
||||||
|
<span class="tool-event-item-title">{{ ev.title }}</span>
|
||||||
|
<span class="tool-event-item-time">{{ formatEventTime(ev.start) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="eventList.length > 5" class="tool-event-more">+{{ eventList.length - 5 }} more</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="calendarList !== null">
|
||||||
|
<div class="tool-calendar-list">
|
||||||
|
<span v-for="name in calendarList" :key="name" class="tool-calendar-name">{{ name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="webSearch">
|
||||||
|
<div class="tool-search-results web-search-results">
|
||||||
|
<a
|
||||||
|
v-for="(r, i) in webSearch.results"
|
||||||
|
:key="i"
|
||||||
|
:href="r.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="tool-search-item"
|
||||||
|
>{{ r.title || r.url }}</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Suggested tags always at the bottom of detail -->
|
||||||
|
<div v-if="suggestedTags.length > 0" class="tag-suggestions">
|
||||||
|
<span class="tag-suggestions-label">Suggested tags:</span>
|
||||||
|
<button
|
||||||
|
v-for="tag in suggestedTags"
|
||||||
|
:key="tag"
|
||||||
|
:class="['tag-pill', { applied: appliedTags.has(tag) }]"
|
||||||
|
:disabled="appliedTags.has(tag) || applyingTag === tag"
|
||||||
|
@click="applyTag(tag)"
|
||||||
|
>
|
||||||
|
#{{ tag }}
|
||||||
|
<span v-if="appliedTags.has(tag)" class="tag-check">✓</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Suggested tags for non-detail cards (note/task created etc.) -->
|
||||||
|
<div v-if="!hasDetail && suggestedTags.length > 0" class="tag-suggestions">
|
||||||
<span class="tag-suggestions-label">Suggested tags:</span>
|
<span class="tag-suggestions-label">Suggested tags:</span>
|
||||||
<button
|
<button
|
||||||
v-for="tag in suggestedTags"
|
v-for="tag in suggestedTags"
|
||||||
@@ -367,16 +421,14 @@ async function applyTag(tag: string) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tool-call-card {
|
.tool-call-card {
|
||||||
display: inline-flex;
|
display: inline-block;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 0.3rem 0.6rem;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
.tool-call-card.error {
|
.tool-call-card.error {
|
||||||
border-color: var(--color-danger, #e74c3c);
|
border-color: var(--color-danger, #e74c3c);
|
||||||
@@ -384,44 +436,83 @@ async function applyTag(tag: string) {
|
|||||||
.tool-call-card.declined {
|
.tool-call-card.declined {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
.tool-declined-name {
|
|
||||||
text-decoration: line-through;
|
/* ── Header ── */
|
||||||
color: var(--color-text-muted);
|
.tool-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
}
|
}
|
||||||
|
.tool-card-header.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.tool-card-header.clickable:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 6%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-label {
|
.tool-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-chevron {
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.4rem;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tool-chevron.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-summary-count {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Detail ── */
|
||||||
|
.tool-card-detail {
|
||||||
|
padding: 0.3rem 0.6rem 0.45rem;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Shared content styles ── */
|
||||||
.tool-link {
|
.tool-link {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.tool-link:hover {
|
.tool-link:hover { text-decoration: underline; }
|
||||||
text-decoration: underline;
|
|
||||||
}
|
.tool-error { color: var(--color-danger, #e74c3c); }
|
||||||
.tool-error {
|
|
||||||
color: var(--color-danger, #e74c3c);
|
.tool-declined-name {
|
||||||
}
|
text-decoration: line-through;
|
||||||
.tool-search-info {
|
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-search-results {
|
.tool-search-results {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.tool-search-item {
|
.tool-search-item {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
.tool-search-item:hover {
|
.tool-search-item:hover { text-decoration: underline; }
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.tool-search-item:not(:last-child)::after {
|
.tool-search-item:not(:last-child)::after {
|
||||||
content: ",";
|
content: ",";
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
@@ -431,131 +522,59 @@ async function applyTag(tag: string) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
.web-search-results .tool-search-item::after {
|
.web-search-results .tool-search-item::after { content: none; }
|
||||||
content: none;
|
|
||||||
}
|
.tool-event-title { font-weight: 600; color: var(--color-text); }
|
||||||
.tool-event-title {
|
.tool-event-time { color: var(--color-text-muted); font-size: 0.75rem; }
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text);
|
.tool-event-list { display: flex; flex-direction: column; gap: 0.2rem; }
|
||||||
}
|
.tool-event-item { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; }
|
||||||
.tool-event-time {
|
.tool-event-item-title { color: var(--color-text); font-size: 0.8rem; }
|
||||||
color: var(--color-text-muted);
|
.tool-event-item-time { color: var(--color-text-muted); font-size: 0.75rem; white-space: nowrap; }
|
||||||
font-size: 0.75rem;
|
.tool-event-more { color: var(--color-text-muted); font-size: 0.75rem; font-style: italic; }
|
||||||
}
|
|
||||||
.tool-event-list {
|
.tool-deleted { text-decoration: line-through; opacity: 0.7; }
|
||||||
display: flex;
|
.tool-completed { text-decoration: line-through; color: var(--color-success, #2ecc71); }
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.2rem;
|
.tool-note-tags { display: flex; flex-wrap: wrap; gap: 0.2rem; }
|
||||||
width: 100%;
|
.tool-note-tag { font-size: 0.72rem; color: var(--color-text-muted); }
|
||||||
}
|
|
||||||
.tool-event-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.tool-event-item-title {
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
.tool-event-item-time {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.tool-event-more {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.tool-deleted {
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.tool-note-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.2rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.tool-note-tag {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
.tool-task-priority {
|
.tool-task-priority {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
|
||||||
font-weight: 600;
|
letter-spacing: 0.04em; padding: 0.1rem 0.3rem; border-radius: 3px;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
padding: 0.1rem 0.3rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
.priority-high { color: var(--color-danger, #e74c3c); }
|
.priority-high { color: var(--color-danger, #e74c3c); }
|
||||||
.priority-medium { color: var(--color-warning, #f59e0b); }
|
.priority-medium { color: var(--color-warning, #f59e0b); }
|
||||||
.priority-low { color: var(--color-text-muted); }
|
.priority-low { color: var(--color-text-muted); }
|
||||||
.tool-completed {
|
|
||||||
text-decoration: line-through;
|
.tool-calendar-list { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
color: var(--color-success, #2ecc71);
|
|
||||||
}
|
|
||||||
.tool-calendar-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
.tool-calendar-name {
|
.tool-calendar-name {
|
||||||
display: inline-block;
|
display: inline-block; padding: 0.1rem 0.5rem;
|
||||||
padding: 0.1rem 0.5rem;
|
border-radius: 999px; background: var(--color-bg-secondary);
|
||||||
border-radius: 999px;
|
border: 1px solid var(--color-border); font-size: 0.75rem; color: var(--color-text);
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tag suggestions */
|
/* ── Tag suggestions ── */
|
||||||
.tag-suggestions {
|
.tag-suggestions {
|
||||||
display: flex;
|
display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem;
|
||||||
flex-wrap: wrap;
|
padding: 0.3rem 0.6rem; border-top: 1px solid var(--color-border);
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
padding-top: 0.3rem;
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
}
|
}
|
||||||
.tag-suggestions-label {
|
.tool-call-card:not(:has(.tool-card-detail)) .tag-suggestions {
|
||||||
font-size: 0.7rem;
|
/* When outside detail, no top border needed if it's the only extra content */
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
.tag-suggestions-label { font-size: 0.7rem; color: var(--color-text-muted); font-weight: 500; }
|
||||||
|
|
||||||
.tag-pill {
|
.tag-pill {
|
||||||
display: inline-flex;
|
display: inline-flex; align-items: center; gap: 0.2rem;
|
||||||
align-items: center;
|
padding: 0.15rem 0.5rem; border: 1px solid var(--color-primary);
|
||||||
gap: 0.2rem;
|
border-radius: 999px; background: transparent; color: var(--color-primary);
|
||||||
padding: 0.15rem 0.5rem;
|
font-size: 0.75rem; cursor: pointer; transition: background 0.15s, color 0.15s;
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.tag-pill:hover:not(:disabled) {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
.tag-pill:hover:not(:disabled) { background: var(--color-primary); color: #fff; }
|
||||||
.tag-pill.applied {
|
.tag-pill.applied {
|
||||||
background: var(--color-success, #2ecc71);
|
background: var(--color-success, #2ecc71); border-color: var(--color-success, #2ecc71);
|
||||||
border-color: var(--color-success, #2ecc71);
|
color: #fff; cursor: default;
|
||||||
color: #fff;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.tag-pill:disabled:not(.applied) {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: wait;
|
|
||||||
}
|
|
||||||
.tag-check {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
}
|
}
|
||||||
|
.tag-pill:disabled:not(.applied) { opacity: 0.6; cursor: wait; }
|
||||||
|
.tag-check { font-size: 0.65rem; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user