feat: event cards in chat/briefing, upcoming events on dashboard, briefing uses internal store
- ToolCallCard: event list items replaced with rich clickable cards (color dot, title, time, location); clicking opens EventSlideOver for edit/delete; single create/update events in header are also clickable; updated all event types to use start_dt/end_dt fields from internal store - HomeView: new upcoming events widget shows today + next 7 days as a card grid above the hero project; clicking any card opens EventSlideOver inline - briefing_pipeline: _gather_internal now queries the internal events store for today's events; CalDAV events are still appended (deduped) if configured Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { apiPost } from "@/api/client";
|
||||
import { apiPost, getEvent } from "@/api/client";
|
||||
import type { EventEntry } from "@/api/client";
|
||||
import type { ToolCallRecord } from "@/types/chat";
|
||||
import EventSlideOver from "@/components/EventSlideOver.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: ToolCallRecord;
|
||||
@@ -67,19 +69,19 @@ const suggestedTags = computed(() => props.toolCall.result.suggested_tags ?? [])
|
||||
const eventData = computed(() => {
|
||||
const data = props.toolCall.result.data;
|
||||
if (!data || props.toolCall.result.type !== "event") return null;
|
||||
return data as { title: string; start: string; end: string };
|
||||
return data as { id?: number; title: string; start_dt?: string; end_dt?: string; location?: string; color?: string };
|
||||
});
|
||||
|
||||
const updatedEvent = computed(() => {
|
||||
const data = props.toolCall.result.data;
|
||||
if (!data || props.toolCall.result.type !== "event_updated") return null;
|
||||
return data as { title: string; start: string; end: string };
|
||||
return data as { id?: number; title: string; start_dt?: string; end_dt?: string; location?: string; color?: string };
|
||||
});
|
||||
|
||||
const deletedEvent = computed(() => {
|
||||
const data = props.toolCall.result.data;
|
||||
if (!data || props.toolCall.result.type !== "event_deleted") return null;
|
||||
return data as { title: string };
|
||||
return data as { id?: number; title: string };
|
||||
});
|
||||
|
||||
const calendarList = computed(() => {
|
||||
@@ -111,7 +113,7 @@ const todoCount = computed(() => {
|
||||
const eventList = computed(() => {
|
||||
const data = props.toolCall.result.data;
|
||||
if (!data || props.toolCall.result.type !== "events") return null;
|
||||
return (data.events as Array<{ title: string; start: string; end: string; location?: string }> | undefined) ?? [];
|
||||
return (data.events as Array<{ id?: number; title: string; start_dt?: string; end_dt?: string; location?: string; color?: string }> | undefined) ?? [];
|
||||
});
|
||||
|
||||
const eventCount = computed(() => {
|
||||
@@ -251,6 +253,26 @@ async function applyTag(tag: string) {
|
||||
applyingTag.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event slide-over ─────────────────────────────────────────────────────────
|
||||
|
||||
const eventSlideOverOpen = ref(false);
|
||||
const eventSlideOverEntry = ref<EventEntry | null>(null);
|
||||
|
||||
async function openEventSlideOver(id: number | undefined) {
|
||||
if (!id) return;
|
||||
try {
|
||||
const entry = await getEvent(id);
|
||||
eventSlideOverEntry.value = entry;
|
||||
eventSlideOverOpen.value = true;
|
||||
} catch {
|
||||
// silently fail — event may have been deleted
|
||||
}
|
||||
}
|
||||
|
||||
function closeEventSlideOver() {
|
||||
eventSlideOverOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -316,12 +338,26 @@ async function applyTag(tag: string) {
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="eventData">
|
||||
<span class="tool-event-title">{{ eventData.title }}</span>
|
||||
<span class="tool-event-time">{{ formatEventTime(eventData.start) }}</span>
|
||||
<button v-if="eventData.id" class="tool-event-btn" @click.stop="openEventSlideOver(eventData.id)">
|
||||
<span class="tool-event-dot" v-if="eventData.color" :style="{ background: eventData.color }"></span>
|
||||
<span class="tool-event-title">{{ eventData.title }}</span>
|
||||
<span v-if="eventData.start_dt" class="tool-event-time">{{ formatEventTime(eventData.start_dt) }}</span>
|
||||
</button>
|
||||
<template v-else>
|
||||
<span class="tool-event-title">{{ eventData.title }}</span>
|
||||
<span v-if="eventData.start_dt" class="tool-event-time">{{ formatEventTime(eventData.start_dt) }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="updatedEvent">
|
||||
<span class="tool-event-title">{{ updatedEvent.title }}</span>
|
||||
<span class="tool-event-time">{{ formatEventTime(updatedEvent.start) }}</span>
|
||||
<button v-if="updatedEvent.id" class="tool-event-btn" @click.stop="openEventSlideOver(updatedEvent.id)">
|
||||
<span class="tool-event-dot" v-if="updatedEvent.color" :style="{ background: updatedEvent.color }"></span>
|
||||
<span class="tool-event-title">{{ updatedEvent.title }}</span>
|
||||
<span v-if="updatedEvent.start_dt" class="tool-event-time">{{ formatEventTime(updatedEvent.start_dt) }}</span>
|
||||
</button>
|
||||
<template v-else>
|
||||
<span class="tool-event-title">{{ updatedEvent.title }}</span>
|
||||
<span v-if="updatedEvent.start_dt" class="tool-event-time">{{ formatEventTime(updatedEvent.start_dt) }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="deletedEvent">
|
||||
<span class="tool-deleted">{{ deletedEvent.title }}</span>
|
||||
@@ -410,11 +446,21 @@ async function applyTag(tag: string) {
|
||||
</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 class="tool-event-cards">
|
||||
<button
|
||||
v-for="(ev, i) in eventList.slice(0, 5)"
|
||||
:key="i"
|
||||
class="tool-event-card"
|
||||
:class="{ clickable: !!ev.id }"
|
||||
@click="openEventSlideOver(ev.id)"
|
||||
>
|
||||
<span class="tool-event-card-dot" :style="ev.color ? { background: ev.color } : {}"></span>
|
||||
<span class="tool-event-card-body">
|
||||
<span class="tool-event-card-title">{{ ev.title }}</span>
|
||||
<span v-if="ev.start_dt" class="tool-event-card-time">{{ formatEventTime(ev.start_dt) }}</span>
|
||||
<span v-if="ev.location" class="tool-event-card-loc">{{ ev.location }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="eventList.length > 5" class="tool-event-more">+{{ eventList.length - 5 }} more</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -469,6 +515,17 @@ async function applyTag(tag: string) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event slide-over (portal-like, fixed positioned) -->
|
||||
<EventSlideOver
|
||||
v-if="eventSlideOverOpen"
|
||||
:event="eventSlideOverEntry"
|
||||
initial-date=""
|
||||
@close="closeEventSlideOver"
|
||||
@created="closeEventSlideOver"
|
||||
@updated="closeEventSlideOver"
|
||||
@deleted="closeEventSlideOver"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -671,4 +728,82 @@ async function applyTag(tag: string) {
|
||||
color: var(--color-danger, #e74c3c);
|
||||
border-color: var(--color-danger, #e74c3c);
|
||||
}
|
||||
|
||||
/* ── Event header click button ── */
|
||||
.tool-event-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.tool-event-btn:hover .tool-event-title {
|
||||
text-decoration: underline;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.tool-event-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary, #6366f1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Event cards (list) ── */
|
||||
.tool-event-cards { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.tool-event-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.45rem;
|
||||
padding: 0.3rem 0.45rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg-card, #16161a);
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
cursor: default;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
width: 100%;
|
||||
}
|
||||
.tool-event-card.clickable { cursor: pointer; }
|
||||
.tool-event-card.clickable:hover {
|
||||
border-color: var(--color-primary, #6366f1);
|
||||
background: color-mix(in srgb, var(--color-primary, #6366f1) 6%, var(--color-bg-card, #16161a));
|
||||
}
|
||||
.tool-event-card-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary, #6366f1);
|
||||
flex-shrink: 0;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.tool-event-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.tool-event-card-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.tool-event-card-time {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.tool-event-card-loc {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user