refactor: migrate HomeView to ChatPanel widget, delete DashboardChatInput
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { apiGet, listEvents } from "@/api/client";
|
||||
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh";
|
||||
import { milestoneColor } from "@/utils/palette";
|
||||
import { fmtRelativeDateTime } from "@/utils/dateFormat";
|
||||
import type { Note } from "@/types/note";
|
||||
import type { Task, TaskListResponse, TaskStatus } from "@/types/task";
|
||||
import type { ToolCallRecord, Message } from "@/types/chat";
|
||||
import type { EventEntry } from "@/api/client";
|
||||
import NoteCard from "@/components/NoteCard.vue";
|
||||
import TaskCard from "@/components/TaskCard.vue";
|
||||
import StatusBadge from "@/components/StatusBadge.vue";
|
||||
import PriorityBadge from "@/components/PriorityBadge.vue";
|
||||
import ToolCallCard from "@/components/ToolCallCard.vue";
|
||||
import DashboardChatInput from "@/components/DashboardChatInput.vue";
|
||||
import ChatPanel from "@/components/ChatPanel.vue";
|
||||
import EventSlideOver from "@/components/EventSlideOver.vue";
|
||||
import { useTasksStore } from "@/stores/tasks";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
@@ -157,7 +155,7 @@ onMounted(async () => {
|
||||
loading.value = false;
|
||||
|
||||
// Focus chat input after data loads
|
||||
chatInputRef.value?.focus();
|
||||
chatPanelRef.value?.focus();
|
||||
loadProjects();
|
||||
|
||||
});
|
||||
@@ -216,10 +214,10 @@ function onStatusToggle(id: number, status: TaskStatus) {
|
||||
|
||||
// ─── Chat widget ──────────────────────────────────────────────────────────────
|
||||
|
||||
const chatInputRef = ref<{ focus: () => void } | null>(null);
|
||||
const chatPanelRef = ref<InstanceType<typeof ChatPanel> | null>(null);
|
||||
|
||||
function onFocusChatShortcut() {
|
||||
chatInputRef.value?.focus();
|
||||
chatPanelRef.value?.focus();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -235,16 +233,6 @@ chatStore.fetchStatus().then(() => {
|
||||
if (chatStore.defaultModel) chatStore.warmModel(chatStore.defaultModel);
|
||||
});
|
||||
|
||||
const dashboardConvId = ref<number | null>(null);
|
||||
const dashboardDone = ref(false);
|
||||
const dashboardQuery = ref("");
|
||||
const dashboardFinalContent = ref("");
|
||||
const dashboardFinalToolCalls = ref<ToolCallRecord[]>([]);
|
||||
|
||||
const isConversational = computed(
|
||||
() => dashboardDone.value && dashboardFinalToolCalls.value.length === 0
|
||||
);
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
"What's due today?",
|
||||
"Events this week?",
|
||||
@@ -252,38 +240,8 @@ const QUICK_ACTIONS = [
|
||||
"My high priority tasks?",
|
||||
];
|
||||
|
||||
async function onChatSubmit(payload: { content: string; contextNoteId?: number }) {
|
||||
dashboardConvId.value = null;
|
||||
dashboardDone.value = false;
|
||||
dashboardFinalContent.value = "";
|
||||
dashboardFinalToolCalls.value = [];
|
||||
dashboardQuery.value = payload.content;
|
||||
|
||||
const conv = await chatStore.createConversation();
|
||||
await chatStore.fetchConversation(conv.id);
|
||||
dashboardConvId.value = conv.id;
|
||||
|
||||
await chatStore.sendMessage(payload.content, payload.contextNoteId);
|
||||
|
||||
const msgs = chatStore.currentConversation?.messages ?? [];
|
||||
const lastAssistant = [...msgs]
|
||||
.reverse()
|
||||
.find((m: Message) => m.role === "assistant");
|
||||
dashboardFinalContent.value = lastAssistant?.content ?? "";
|
||||
dashboardFinalToolCalls.value = (lastAssistant?.tool_calls ?? []) as ToolCallRecord[];
|
||||
dashboardDone.value = true;
|
||||
}
|
||||
|
||||
async function onQuickAction(query: string) {
|
||||
await onChatSubmit({ content: query });
|
||||
}
|
||||
|
||||
function clearDashboardResponse() {
|
||||
dashboardConvId.value = null;
|
||||
dashboardDone.value = false;
|
||||
dashboardQuery.value = "";
|
||||
dashboardFinalContent.value = "";
|
||||
dashboardFinalToolCalls.value = [];
|
||||
await chatPanelRef.value?.send(query);
|
||||
}
|
||||
|
||||
// ─── Upcoming events slide-over ───────────────────────────────────────────────
|
||||
@@ -324,38 +282,9 @@ function formatUpcomingTime(event: EventEntry): string {
|
||||
@click="onQuickAction(q)"
|
||||
>{{ q }}</button>
|
||||
</div>
|
||||
<DashboardChatInput ref="chatInputRef" @submit="onChatSubmit" />
|
||||
<ChatPanel ref="chatPanelRef" variant="widget" />
|
||||
</section>
|
||||
|
||||
<!-- ── Inline response ────────────────────────────────────── -->
|
||||
<div v-if="dashboardConvId" class="dashboard-response">
|
||||
<div class="dashboard-response-query">{{ dashboardQuery }}</div>
|
||||
<div v-if="chatStore.streaming && chatStore.streamingToolCalls.length" class="dashboard-tool-calls">
|
||||
<ToolCallCard v-for="(tc, i) in chatStore.streamingToolCalls" :key="i" :tool-call="tc" />
|
||||
</div>
|
||||
<div v-else-if="dashboardDone && dashboardFinalToolCalls.length" class="dashboard-tool-calls">
|
||||
<ToolCallCard v-for="(tc, i) in dashboardFinalToolCalls" :key="i" :tool-call="tc" />
|
||||
</div>
|
||||
<div v-if="chatStore.streaming" class="dashboard-response-text streaming">
|
||||
<div v-if="chatStore.streamingStatus && !chatStore.streamingContent" class="dashboard-status-line">
|
||||
<span class="dashboard-status-dot"></span>{{ chatStore.streamingStatus }}
|
||||
</div>
|
||||
<span v-else-if="chatStore.streamingContent">{{ chatStore.streamingContent }}</span>
|
||||
<span v-else class="thinking-dots">...</span>
|
||||
</div>
|
||||
<div v-else-if="dashboardDone && dashboardFinalContent" class="dashboard-response-text">
|
||||
{{ dashboardFinalContent }}
|
||||
</div>
|
||||
<div class="dashboard-response-actions" :class="{ conversational: isConversational }">
|
||||
<router-link
|
||||
:to="`/chat/${dashboardConvId}`"
|
||||
class="btn-open-chat"
|
||||
:class="{ prominent: isConversational }"
|
||||
>{{ isConversational ? 'Continue the conversation →' : 'Think it through in Chat →' }}</router-link>
|
||||
<button class="btn-clear-response" @click="clearDashboardResponse">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Upcoming events ────────────────────────────────────── -->
|
||||
<div v-if="!loading && upcomingEvents.length" class="upcoming-events-section">
|
||||
<div class="section-header">
|
||||
@@ -608,92 +537,6 @@ function formatUpcomingTime(event: EventEntry): string {
|
||||
}
|
||||
.quick-action-chip:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ─── Inline response ────────────────────────────────────────── */
|
||||
.dashboard-response {
|
||||
max-width: 720px;
|
||||
margin: 0 auto 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--color-bg-card);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--color-bubble-asst-shadow);
|
||||
}
|
||||
.dashboard-response-query {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.dashboard-tool-calls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.dashboard-response-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.dashboard-response-text.streaming { color: var(--color-text-muted); }
|
||||
.thinking-dots { display: inline-block; animation: blink 1.2s infinite; }
|
||||
.dashboard-status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-style: italic;
|
||||
}
|
||||
.dashboard-status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
animation: blink 1.2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
.dashboard-response-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
.btn-open-chat {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-open-chat:hover { text-decoration: underline; }
|
||||
.btn-open-chat.prominent {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||||
color: #fff;
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 1px 6px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
.btn-open-chat.prominent:hover {
|
||||
text-decoration: none;
|
||||
box-shadow: 0 3px 12px rgba(99, 102, 241, 0.45);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
.btn-clear-response {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
.btn-clear-response:hover { color: var(--color-text); }
|
||||
|
||||
/* ─── Skeleton loading ───────────────────────────────────────── */
|
||||
.skeleton-hero {
|
||||
height: 180px;
|
||||
|
||||
Reference in New Issue
Block a user