Redesign dashboard: chat-first layout, two-column task/notes grid
- Remove page title; move chat widget (quick actions + input) to top full-width - Remove recent chats section beneath the widget - Inline streaming response stays full-width between widget and grid - Two-column grid below (3fr tasks / 2fr notes, collapses to 1 col on mobile) - Left column: all active tasks categorized by urgency — Overdue, Due Today, Due This Week, High Priority, In Progress, then Other (capped at 10) - Other section: broad fetch of all non-done tasks deduped against shown sections; sorted due-dated items first (asc), then undated by priority - Right column: recent notes bumped from 5 to 8 - Max-width increased from 1200px to 1400px - Section labels styled as small uppercase headings; overdue/high-priority retain colored left-border treatment - Single "See all" link per column header instead of per-section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+264
-264
@@ -3,7 +3,7 @@ import { ref, computed, onMounted, nextTick } from "vue";
|
|||||||
import { apiGet } from "@/api/client";
|
import { apiGet } from "@/api/client";
|
||||||
import type { Note, NoteListResponse } from "@/types/note";
|
import type { Note, NoteListResponse } from "@/types/note";
|
||||||
import type { Task, TaskListResponse } from "@/types/task";
|
import type { Task, TaskListResponse } from "@/types/task";
|
||||||
import type { Conversation, ToolCallRecord, Message } from "@/types/chat";
|
import type { ToolCallRecord, Message } from "@/types/chat";
|
||||||
import NoteCard from "@/components/NoteCard.vue";
|
import NoteCard from "@/components/NoteCard.vue";
|
||||||
import TaskCard from "@/components/TaskCard.vue";
|
import TaskCard from "@/components/TaskCard.vue";
|
||||||
import ToolCallCard from "@/components/ToolCallCard.vue";
|
import ToolCallCard from "@/components/ToolCallCard.vue";
|
||||||
@@ -18,7 +18,7 @@ const dueTodayTasks = ref<Task[]>([]);
|
|||||||
const dueThisWeekTasks = ref<Task[]>([]);
|
const dueThisWeekTasks = ref<Task[]>([]);
|
||||||
const highPriorityTasks = ref<Task[]>([]);
|
const highPriorityTasks = ref<Task[]>([]);
|
||||||
const inProgressTasks = ref<Task[]>([]);
|
const inProgressTasks = ref<Task[]>([]);
|
||||||
const recentChats = ref<Conversation[]>([]);
|
const otherTasks = ref<Task[]>([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const tasksStore = useTasksStore();
|
const tasksStore = useTasksStore();
|
||||||
|
|
||||||
@@ -46,7 +46,6 @@ function sortByPriorityThenDate(tasks: Task[]): Task[] {
|
|||||||
const pa = PRIORITY_ORDER[a.priority ?? "none"] ?? 0;
|
const pa = PRIORITY_ORDER[a.priority ?? "none"] ?? 0;
|
||||||
const pb = PRIORITY_ORDER[b.priority ?? "none"] ?? 0;
|
const pb = PRIORITY_ORDER[b.priority ?? "none"] ?? 0;
|
||||||
if (pb !== pa) return pb - pa;
|
if (pb !== pa) return pb - pa;
|
||||||
// Earlier due dates first; null due dates last
|
|
||||||
if (a.due_date && b.due_date) return a.due_date.localeCompare(b.due_date);
|
if (a.due_date && b.due_date) return a.due_date.localeCompare(b.due_date);
|
||||||
if (a.due_date) return -1;
|
if (a.due_date) return -1;
|
||||||
if (b.due_date) return 1;
|
if (b.due_date) return 1;
|
||||||
@@ -54,13 +53,27 @@ function sortByPriorityThenDate(tasks: Task[]): Task[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortOtherTasks(tasks: Task[]): Task[] {
|
||||||
|
const withDate = tasks
|
||||||
|
.filter((t) => t.due_date)
|
||||||
|
.sort((a, b) => a.due_date!.localeCompare(b.due_date!));
|
||||||
|
const withoutDate = tasks
|
||||||
|
.filter((t) => !t.due_date)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const pa = PRIORITY_ORDER[a.priority ?? "none"] ?? 0;
|
||||||
|
const pb = PRIORITY_ORDER[b.priority ?? "none"] ?? 0;
|
||||||
|
return pb - pa;
|
||||||
|
});
|
||||||
|
return [...withDate, ...withoutDate].slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const today = dateStr(0);
|
const today = dateStr(0);
|
||||||
const nextWeek = dateStr(7);
|
const nextWeek = dateStr(7);
|
||||||
|
|
||||||
const [notesRes, overdueRes, dueSoonRes, highPriRes, inProgressRes, chatsRes] =
|
const [notesRes, overdueRes, dueSoonRes, highPriRes, inProgressRes, otherRes] =
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
apiGet<NoteListResponse>("/api/notes?sort=updated_at&order=desc&limit=5"),
|
apiGet<NoteListResponse>("/api/notes?sort=updated_at&order=desc&limit=8"),
|
||||||
apiGet<TaskListResponse>(
|
apiGet<TaskListResponse>(
|
||||||
`/api/tasks?due_before=${today}&sort=due_date&order=asc&limit=20`
|
`/api/tasks?due_before=${today}&sort=due_date&order=asc&limit=20`
|
||||||
),
|
),
|
||||||
@@ -73,22 +86,18 @@ onMounted(async () => {
|
|||||||
apiGet<TaskListResponse>(
|
apiGet<TaskListResponse>(
|
||||||
`/api/tasks?status=in_progress&sort=updated_at&order=desc&limit=10`
|
`/api/tasks?status=in_progress&sort=updated_at&order=desc&limit=10`
|
||||||
),
|
),
|
||||||
apiGet<{ conversations: Conversation[]; total: number }>(
|
apiGet<TaskListResponse>(
|
||||||
"/api/chat/conversations?limit=3&offset=0"
|
`/api/tasks?sort=updated_at&order=desc&limit=50`
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (notesRes.status === "fulfilled") {
|
if (notesRes.status === "fulfilled") {
|
||||||
recentNotes.value = notesRes.value.notes;
|
recentNotes.value = notesRes.value.notes;
|
||||||
}
|
}
|
||||||
if (chatsRes.status === "fulfilled") {
|
|
||||||
recentChats.value = chatsRes.value.conversations;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build seen-set incrementally so each section deduplicates against all above it
|
// Build seen-set incrementally so each section deduplicates against all above it
|
||||||
const seen = new Set<number>();
|
const seen = new Set<number>();
|
||||||
|
|
||||||
// Overdue: past due, not done
|
|
||||||
if (overdueRes.status === "fulfilled") {
|
if (overdueRes.status === "fulfilled") {
|
||||||
overdueTasks.value = sortByPriorityThenDate(
|
overdueTasks.value = sortByPriorityThenDate(
|
||||||
overdueRes.value.tasks.filter((t) => t.status !== "done")
|
overdueRes.value.tasks.filter((t) => t.status !== "done")
|
||||||
@@ -96,7 +105,6 @@ onMounted(async () => {
|
|||||||
for (const t of overdueTasks.value) seen.add(t.id);
|
for (const t of overdueTasks.value) seen.add(t.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due soon: split into "today" and "this week", exclude done
|
|
||||||
if (dueSoonRes.status === "fulfilled") {
|
if (dueSoonRes.status === "fulfilled") {
|
||||||
const notDone = dueSoonRes.value.tasks.filter(
|
const notDone = dueSoonRes.value.tasks.filter(
|
||||||
(t) => t.status !== "done" && !seen.has(t.id)
|
(t) => t.status !== "done" && !seen.has(t.id)
|
||||||
@@ -116,7 +124,6 @@ onMounted(async () => {
|
|||||||
for (const t of dueThisWeekTasks.value) seen.add(t.id);
|
for (const t of dueThisWeekTasks.value) seen.add(t.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// High priority: not done, deduplicated
|
|
||||||
if (highPriRes.status === "fulfilled") {
|
if (highPriRes.status === "fulfilled") {
|
||||||
highPriorityTasks.value = highPriRes.value.tasks.filter(
|
highPriorityTasks.value = highPriRes.value.tasks.filter(
|
||||||
(t) => t.status !== "done" && !seen.has(t.id)
|
(t) => t.status !== "done" && !seen.has(t.id)
|
||||||
@@ -124,23 +131,41 @@ onMounted(async () => {
|
|||||||
for (const t of highPriorityTasks.value) seen.add(t.id);
|
for (const t of highPriorityTasks.value) seen.add(t.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In progress: deduplicated against all above
|
|
||||||
if (inProgressRes.status === "fulfilled") {
|
if (inProgressRes.status === "fulfilled") {
|
||||||
inProgressTasks.value = inProgressRes.value.tasks.filter(
|
inProgressTasks.value = inProgressRes.value.tasks.filter(
|
||||||
(t) => !seen.has(t.id)
|
(t) => !seen.has(t.id)
|
||||||
);
|
);
|
||||||
|
for (const t of inProgressTasks.value) seen.add(t.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherRes.status === "fulfilled") {
|
||||||
|
const remaining = otherRes.value.tasks.filter(
|
||||||
|
(t) => t.status !== "done" && !seen.has(t.id)
|
||||||
|
);
|
||||||
|
otherTasks.value = sortOtherTasks(remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
nextTick(() => chatInputRef.value?.focus());
|
nextTick(() => chatInputRef.value?.focus());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const noTasks = computed(
|
||||||
|
() =>
|
||||||
|
overdueTasks.value.length === 0 &&
|
||||||
|
dueTodayTasks.value.length === 0 &&
|
||||||
|
dueThisWeekTasks.value.length === 0 &&
|
||||||
|
highPriorityTasks.value.length === 0 &&
|
||||||
|
inProgressTasks.value.length === 0 &&
|
||||||
|
otherTasks.value.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
const allTaskLists = [
|
const allTaskLists = [
|
||||||
() => overdueTasks,
|
() => overdueTasks,
|
||||||
() => dueTodayTasks,
|
() => dueTodayTasks,
|
||||||
() => dueThisWeekTasks,
|
() => dueThisWeekTasks,
|
||||||
() => highPriorityTasks,
|
() => highPriorityTasks,
|
||||||
() => inProgressTasks,
|
() => inProgressTasks,
|
||||||
|
() => otherTasks,
|
||||||
];
|
];
|
||||||
|
|
||||||
function removeFromAllLists(id: number) {
|
function removeFromAllLists(id: number) {
|
||||||
@@ -168,24 +193,20 @@ function onStatusToggle(id: number, status: TaskStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatInputRef = ref<{ focus: () => void } | null>(null);
|
const chatInputRef = ref<{ focus: () => void } | null>(null);
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
// Warm default model after status fetch
|
|
||||||
chatStore.fetchStatus().then(() => {
|
chatStore.fetchStatus().then(() => {
|
||||||
if (chatStore.defaultModel) {
|
if (chatStore.defaultModel) {
|
||||||
chatStore.warmModel(chatStore.defaultModel);
|
chatStore.warmModel(chatStore.defaultModel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dashboard inline response state
|
|
||||||
const dashboardConvId = ref<number | null>(null);
|
const dashboardConvId = ref<number | null>(null);
|
||||||
const dashboardDone = ref(false);
|
const dashboardDone = ref(false);
|
||||||
const dashboardQuery = ref("");
|
const dashboardQuery = ref("");
|
||||||
const dashboardFinalContent = ref("");
|
const dashboardFinalContent = ref("");
|
||||||
const dashboardFinalToolCalls = ref<ToolCallRecord[]>([]);
|
const dashboardFinalToolCalls = ref<ToolCallRecord[]>([]);
|
||||||
|
|
||||||
// True when the response is pure text with no tool actions — suggest continuing in chat
|
|
||||||
const isConversational = computed(
|
const isConversational = computed(
|
||||||
() => dashboardDone.value && dashboardFinalToolCalls.value.length === 0
|
() => dashboardDone.value && dashboardFinalToolCalls.value.length === 0
|
||||||
);
|
);
|
||||||
@@ -208,10 +229,8 @@ async function onChatSubmit(payload: { content: string; contextNoteId?: number }
|
|||||||
await chatStore.fetchConversation(conv.id);
|
await chatStore.fetchConversation(conv.id);
|
||||||
dashboardConvId.value = conv.id;
|
dashboardConvId.value = conv.id;
|
||||||
|
|
||||||
// Stream inline — sendMessage awaits until SSE is complete
|
|
||||||
await chatStore.sendMessage(payload.content, payload.contextNoteId);
|
await chatStore.sendMessage(payload.content, payload.contextNoteId);
|
||||||
|
|
||||||
// Capture final response from the message pushed by the store's done handler
|
|
||||||
const msgs = chatStore.currentConversation?.messages ?? [];
|
const msgs = chatStore.currentConversation?.messages ?? [];
|
||||||
const lastAssistant = [...msgs].reverse().find((m: Message) => m.role === "assistant");
|
const lastAssistant = [...msgs].reverse().find((m: Message) => m.role === "assistant");
|
||||||
dashboardFinalContent.value = lastAssistant?.content ?? "";
|
dashboardFinalContent.value = lastAssistant?.content ?? "";
|
||||||
@@ -234,176 +253,165 @@ function clearDashboardResponse() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="home">
|
<main class="home">
|
||||||
<h1>Fabled Assistant</h1>
|
|
||||||
|
<!-- Chat widget — full width at top -->
|
||||||
|
<section class="chat-section">
|
||||||
|
<div class="quick-actions">
|
||||||
|
<button
|
||||||
|
v-for="q in QUICK_ACTIONS"
|
||||||
|
:key="q"
|
||||||
|
class="quick-action-chip"
|
||||||
|
:disabled="chatStore.streaming || !chatStore.chatReady"
|
||||||
|
@click="onQuickAction(q)"
|
||||||
|
>
|
||||||
|
{{ q }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<DashboardChatInput ref="chatInputRef" @submit="onChatSubmit" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Inline response — full width -->
|
||||||
|
<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>
|
||||||
|
|
||||||
<p v-if="loading" class="loading">Loading...</p>
|
<p v-if="loading" class="loading">Loading...</p>
|
||||||
|
|
||||||
<template v-else>
|
<!-- Two-column grid -->
|
||||||
<section v-if="overdueTasks.length" class="section section-overdue">
|
<div v-else class="dashboard-grid">
|
||||||
<div class="section-header">
|
|
||||||
<h2>Overdue</h2>
|
<!-- Left: Tasks -->
|
||||||
|
<div class="col-tasks">
|
||||||
|
<div class="col-header">
|
||||||
|
<h2>Tasks</h2>
|
||||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
<router-link to="/tasks" class="see-all">See all</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="cards">
|
|
||||||
<TaskCard
|
|
||||||
v-for="task in overdueTasks"
|
|
||||||
:key="task.id"
|
|
||||||
:task="task"
|
|
||||||
@status-toggle="onStatusToggle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="dueTodayTasks.length" class="section">
|
<div v-if="noTasks" class="empty-state">
|
||||||
<div class="section-header">
|
<p class="empty-text">No active tasks.</p>
|
||||||
<h2>Due Today</h2>
|
<router-link to="/tasks/new" class="btn-cta">+ New Task</router-link>
|
||||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="cards">
|
|
||||||
<TaskCard
|
|
||||||
v-for="task in dueTodayTasks"
|
|
||||||
:key="task.id"
|
|
||||||
:task="task"
|
|
||||||
@status-toggle="onStatusToggle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="dueThisWeekTasks.length" class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Due This Week</h2>
|
|
||||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="cards">
|
|
||||||
<TaskCard
|
|
||||||
v-for="task in dueThisWeekTasks"
|
|
||||||
:key="task.id"
|
|
||||||
:task="task"
|
|
||||||
@status-toggle="onStatusToggle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="highPriorityTasks.length" class="section section-high-priority">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>High Priority</h2>
|
|
||||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="cards">
|
|
||||||
<TaskCard
|
|
||||||
v-for="task in highPriorityTasks"
|
|
||||||
:key="task.id"
|
|
||||||
:task="task"
|
|
||||||
@status-toggle="onStatusToggle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="inProgressTasks.length" class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>In Progress</h2>
|
|
||||||
<router-link to="/tasks" class="see-all">See all</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="cards">
|
|
||||||
<TaskCard
|
|
||||||
v-for="task in inProgressTasks"
|
|
||||||
:key="task.id"
|
|
||||||
:task="task"
|
|
||||||
@status-toggle="onStatusToggle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Ask Fable</h2>
|
|
||||||
<router-link to="/chat" class="see-all">All chats</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick action chips -->
|
<template v-else>
|
||||||
<div class="quick-actions">
|
<section v-if="overdueTasks.length" class="section section-overdue">
|
||||||
<button
|
<h3 class="section-title">Overdue</h3>
|
||||||
v-for="q in QUICK_ACTIONS"
|
<div class="cards">
|
||||||
:key="q"
|
<TaskCard
|
||||||
class="quick-action-chip"
|
v-for="task in overdueTasks"
|
||||||
:disabled="chatStore.streaming || !chatStore.chatReady"
|
:key="task.id"
|
||||||
@click="onQuickAction(q)"
|
:task="task"
|
||||||
>
|
@status-toggle="onStatusToggle"
|
||||||
{{ q }}
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DashboardChatInput ref="chatInputRef" @submit="onChatSubmit" />
|
|
||||||
|
|
||||||
<!-- Inline streaming response -->
|
|
||||||
<div v-if="dashboardConvId" class="dashboard-response">
|
|
||||||
<div class="dashboard-response-query">{{ dashboardQuery }}</div>
|
|
||||||
|
|
||||||
<!-- Tool calls — show live during streaming, final after done -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Streaming text -->
|
|
||||||
<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>
|
</div>
|
||||||
<span v-else-if="chatStore.streamingContent">{{ chatStore.streamingContent }}</span>
|
</section>
|
||||||
<span v-else class="thinking-dots">...</span>
|
|
||||||
</div>
|
|
||||||
<!-- Final text -->
|
|
||||||
<div v-else-if="dashboardDone && dashboardFinalContent" class="dashboard-response-text">
|
|
||||||
{{ dashboardFinalContent }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-response-actions" :class="{ conversational: isConversational }">
|
<section v-if="dueTodayTasks.length" class="section">
|
||||||
<router-link
|
<h3 class="section-title">Due Today</h3>
|
||||||
:to="`/chat/${dashboardConvId}`"
|
<div class="cards">
|
||||||
class="btn-open-chat"
|
<TaskCard
|
||||||
:class="{ prominent: isConversational }"
|
v-for="task in dueTodayTasks"
|
||||||
>
|
:key="task.id"
|
||||||
{{ isConversational ? 'Continue the conversation →' : 'Think it through in Chat →' }}
|
:task="task"
|
||||||
</router-link>
|
@status-toggle="onStatusToggle"
|
||||||
<button class="btn-clear" @click="clearDashboardResponse">Clear</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Recent chats -->
|
<section v-if="dueThisWeekTasks.length" class="section">
|
||||||
<div v-if="recentChats.length" class="recent-chats">
|
<h3 class="section-title">Due This Week</h3>
|
||||||
<div class="recent-chats-label">Recent</div>
|
<div class="cards">
|
||||||
<div class="cards">
|
<TaskCard
|
||||||
<router-link
|
v-for="task in dueThisWeekTasks"
|
||||||
v-for="chat in recentChats"
|
:key="task.id"
|
||||||
:key="chat.id"
|
:task="task"
|
||||||
:to="`/chat/${chat.id}`"
|
@status-toggle="onStatusToggle"
|
||||||
class="chat-card"
|
/>
|
||||||
>
|
</div>
|
||||||
<span class="chat-card-title">{{ chat.title || "Untitled" }}</span>
|
</section>
|
||||||
<span class="chat-card-meta">{{ chat.message_count }} messages</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
<section v-if="highPriorityTasks.length" class="section section-high-priority">
|
||||||
<div class="section-header">
|
<h3 class="section-title">High Priority</h3>
|
||||||
<h2>Recently Edited</h2>
|
<div class="cards">
|
||||||
|
<TaskCard
|
||||||
|
v-for="task in highPriorityTasks"
|
||||||
|
:key="task.id"
|
||||||
|
:task="task"
|
||||||
|
@status-toggle="onStatusToggle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="inProgressTasks.length" class="section">
|
||||||
|
<h3 class="section-title">In Progress</h3>
|
||||||
|
<div class="cards">
|
||||||
|
<TaskCard
|
||||||
|
v-for="task in inProgressTasks"
|
||||||
|
:key="task.id"
|
||||||
|
:task="task"
|
||||||
|
@status-toggle="onStatusToggle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="otherTasks.length" class="section">
|
||||||
|
<h3 class="section-title">Other</h3>
|
||||||
|
<div class="cards">
|
||||||
|
<TaskCard
|
||||||
|
v-for="task in otherTasks"
|
||||||
|
:key="task.id"
|
||||||
|
:task="task"
|
||||||
|
@status-toggle="onStatusToggle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Notes -->
|
||||||
|
<div class="col-notes">
|
||||||
|
<div class="col-header">
|
||||||
|
<h2>Recent Notes</h2>
|
||||||
<router-link to="/notes" class="see-all">See all</router-link>
|
<router-link to="/notes" class="see-all">See all</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="recentNotes.length" class="cards">
|
<div v-if="recentNotes.length" class="cards">
|
||||||
@@ -417,87 +425,23 @@ function clearDashboardResponse() {
|
|||||||
<p class="empty-text">No notes yet.</p>
|
<p class="empty-text">No notes yet.</p>
|
||||||
<router-link to="/notes/new" class="btn-cta">+ New Note</router-link>
|
<router-link to="/notes/new" class="btn-cta">+ New Note</router-link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.home {
|
.home {
|
||||||
max-width: 1200px;
|
max-width: 1400px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
.home h1 {
|
|
||||||
margin: 0 0 1.5rem;
|
/* Chat section */
|
||||||
|
.chat-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.loading {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
.section {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.section-overdue {
|
|
||||||
border-left: 3px solid var(--color-overdue, #e74c3c);
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
}
|
|
||||||
.section-high-priority {
|
|
||||||
border-left: 3px solid var(--color-warning, #f59e0b);
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
}
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
.section-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
}
|
|
||||||
.see-all {
|
|
||||||
color: var(--color-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.see-all:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.cards {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.chat-card {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--color-text);
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
.chat-card:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
.chat-card-title {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
.chat-card-meta {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
/* Quick actions */
|
|
||||||
.quick-actions {
|
.quick-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -525,7 +469,7 @@ function clearDashboardResponse() {
|
|||||||
|
|
||||||
/* Inline response */
|
/* Inline response */
|
||||||
.dashboard-response {
|
.dashboard-response {
|
||||||
margin-top: 0.75rem;
|
margin-bottom: 1.5rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -604,7 +548,7 @@ function clearDashboardResponse() {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
.btn-clear {
|
.btn-clear-response {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
background: none;
|
background: none;
|
||||||
@@ -612,23 +556,72 @@ function clearDashboardResponse() {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.btn-clear:hover {
|
.btn-clear-response:hover {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Recent chats below */
|
/* Two-column grid */
|
||||||
.recent-chats {
|
.dashboard-grid {
|
||||||
margin-top: 1rem;
|
display: grid;
|
||||||
}
|
grid-template-columns: 3fr 2fr;
|
||||||
.recent-chats-label {
|
gap: 2rem;
|
||||||
font-size: 0.75rem;
|
align-items: start;
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Column headers */
|
||||||
|
.col-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.col-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.see-all {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.see-all:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task sections */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.section-overdue {
|
||||||
|
border-left: 3px solid var(--color-danger, #e74c3c);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
.section-overdue .section-title {
|
||||||
|
color: var(--color-danger, #e74c3c);
|
||||||
|
}
|
||||||
|
.section-high-priority {
|
||||||
|
border-left: 3px solid var(--color-warning, #f59e0b);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty states */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1.5rem 0;
|
padding: 1.5rem 0;
|
||||||
@@ -648,4 +641,11 @@ function clearDashboardResponse() {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user