diff --git a/frontend/src/components/WorkspaceNoteEditor.vue b/frontend/src/components/WorkspaceNoteEditor.vue index c90bac5..4c02c2f 100644 --- a/frontend/src/components/WorkspaceNoteEditor.vue +++ b/frontend/src/components/WorkspaceNoteEditor.vue @@ -28,23 +28,18 @@ interface NoteItem { const toast = useToastStore(); const notesStore = useNotesStore(); -// View state: "list" or "editor" -const view = ref<"list" | "editor">("list"); - // List state const projectNotes = ref([]); const listLoading = ref(false); -const deletingId = ref(null); // confirming delete for this id -const pendingDelete = ref(null); // mid-deletion +const deletingId = ref(null); +const pendingDelete = ref(null); const searchQuery = ref(""); const filteredNotes = computed(() => { const q = searchQuery.value.trim().toLowerCase(); if (!q) return projectNotes.value; return projectNotes.value.filter( - (n) => - n.title.toLowerCase().includes(q) || - n.tags?.some((t) => t.toLowerCase().includes(q)) + (n) => n.title.toLowerCase().includes(q) || n.tags?.some((t) => t.toLowerCase().includes(q)) ); }); @@ -72,6 +67,8 @@ const newNoteTitle = ref(""); const creatingNote = ref(false); const newNoteTitleRef = ref(null); +function _noteKey(pid: number) { return `workspace_note_${pid}`; } + async function startNewNote() { showNewNoteInput.value = true; newNoteTitle.value = ""; @@ -104,7 +101,7 @@ async function createNote() { } } -// TipTap editor ref (for MarkdownToolbar) +// TipTap editor ref const titleRef = ref(null); const editorRef = ref | null>(null); const tiptapEditor = computed(() => @@ -184,7 +181,7 @@ async function openNote(id: number) { dirty.value = false; tagSuggestions.dismissTagSuggestions(); linkSuggestions.value = []; - view.value = "editor"; + localStorage.setItem(_noteKey(props.projectId), String(id)); fetchLinkSuggestions(); emit("note-loaded", note.id); } catch { @@ -202,7 +199,6 @@ async function saveNote() { tags: noteTags.value, }); dirty.value = false; - // Refresh updated_at in the list const idx = projectNotes.value.findIndex((n) => n.id === editingId.value); if (idx !== -1) { projectNotes.value[idx] = { @@ -211,7 +207,6 @@ async function saveNote() { tags: noteTags.value, updated_at: new Date().toISOString(), }; - // Re-sort by updated_at projectNotes.value.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() ); @@ -223,16 +218,9 @@ async function saveNote() { } } -function backToList() { - view.value = "list"; - deletingId.value = null; - searchQuery.value = ""; -} - function requestDelete(id: number, e: Event) { e.stopPropagation(); if (deletingId.value === id) { - // Second click — confirm confirmDelete(id); } else { deletingId.value = id; @@ -253,7 +241,7 @@ async function confirmDelete(id: number) { editingId.value = null; noteTitle.value = ""; noteBody.value = ""; - view.value = "list"; + localStorage.removeItem(_noteKey(props.projectId)); } } catch { toast.show("Failed to delete note", "error"); @@ -292,93 +280,27 @@ watch( ); useAutoSave(dirty, saving, saveNote, 60_000); -onMounted(loadProjectNotes); + +onMounted(async () => { + await loadProjectNotes(); + const stored = localStorage.getItem(_noteKey(props.projectId)); + if (stored) { + const id = Number(stored); + if (projectNotes.value.find((n) => n.id === id)) { + openNote(id).catch(() => {}); + } + } +}); + onUnmounted(() => { if (linkCheckTimer) clearTimeout(linkCheckTimer); }); @@ -467,41 +446,278 @@ onUnmounted(() => { if (linkCheckTimer) clearTimeout(linkCheckTimer); }); diff --git a/frontend/src/components/WorkspaceTaskPanel.vue b/frontend/src/components/WorkspaceTaskPanel.vue index db839d5..f57fc62 100644 --- a/frontend/src/components/WorkspaceTaskPanel.vue +++ b/frontend/src/components/WorkspaceTaskPanel.vue @@ -56,6 +56,17 @@ const STATUS_ICON: Record = { done: "✓", }; +const PRIORITY_CLASS: Record = { + high: "priority-high", + medium: "priority-medium", + low: "priority-low", +}; + +function isRowOverdue(task: Task): boolean { + if (!task.due_date || task.status === "done") return false; + return task.due_date < new Date().toISOString().slice(0, 10); +} + // Group tasks by milestone, "No Milestone" first const groupedTasks = computed(() => { const noMs = tasks.value.filter((t) => t.milestone_id === null); @@ -207,45 +218,99 @@ defineExpose({ reload: loadAll }); @@ -370,9 +351,16 @@ defineExpose({ reload: loadAll }); .task-list-view { display: flex; flex-direction: column; - height: 100%; + flex: 1; + min-height: 0; overflow: hidden; } +.task-list-view.has-detail { + flex: 0 0 44%; +} +.task-active { + background: color-mix(in srgb, var(--color-primary) 6%, var(--color-surface)) !important; +} .panel-header { padding: 0.6rem 0.75rem; @@ -510,15 +498,15 @@ defineExpose({ reload: loadAll }); .state-msg { padding: 1.5rem; text-align: center; font-size: 0.85rem; color: var(--color-text-muted); } -/* ── Slide-over detail ── */ +/* ── Detail pane (bottom split) ── */ .task-detail { - position: absolute; - inset: 0; - background: var(--color-surface); + flex: 1; + min-height: 0; display: flex; flex-direction: column; overflow: hidden; - z-index: 5; + background: var(--color-surface); + border-top: 2px solid var(--color-border); } .detail-header { @@ -530,17 +518,6 @@ defineExpose({ reload: loadAll }); flex-shrink: 0; } -.btn-back-arrow { - background: none; - border: none; - color: var(--color-primary); - font-size: 0.85rem; - cursor: pointer; - padding: 0; - flex-shrink: 0; -} -.btn-back-arrow:hover { text-decoration: underline; } - .status-badge { padding: 0.2rem 0.55rem; border-radius: 12px; @@ -669,11 +646,45 @@ defineExpose({ reload: loadAll }); border-top: 1px solid var(--color-border); } -/* Slide transition */ -.slide-enter-active, -.slide-leave-active { - transition: transform 0.2s ease; +/* Detail fade transition */ +.detail-fade-enter-active, +.detail-fade-leave-active { transition: opacity 0.15s; } +.detail-fade-enter-from, +.detail-fade-leave-to { opacity: 0; } + +/* Priority dots on task rows */ +.priority-dot { + width: 6px; + height: 6px; + border-radius: 2px; + flex-shrink: 0; } -.slide-enter-from { transform: translateX(100%); } -.slide-leave-to { transform: translateX(100%); } +.priority-high { background: #ef4444; } +.priority-medium { background: #f59e0b; } +.priority-low { background: #3b82f6; } + +/* Due date on task rows */ +.task-due { + font-size: 0.65rem; + color: var(--color-text-muted); + white-space: nowrap; + flex-shrink: 0; +} +.task-due.overdue { + color: var(--color-danger, #e74c3c); + font-weight: 600; +} + +/* Close detail button */ +.btn-close-detail { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 0.75rem; + cursor: pointer; + padding: 0.15rem 0.3rem; + border-radius: 3px; + margin-left: 0.2rem; +} +.btn-close-detail:hover { color: var(--color-text); } diff --git a/frontend/src/views/WorkspaceView.vue b/frontend/src/views/WorkspaceView.vue index de73437..bf05ec0 100644 --- a/frontend/src/views/WorkspaceView.vue +++ b/frontend/src/views/WorkspaceView.vue @@ -22,6 +22,7 @@ const projectId = computed(() => Number(route.params.projectId)); interface Project { id: number; title: string; + goal?: string; } const project = ref(null); @@ -56,12 +57,11 @@ function _loadPanelState(pid: number) { const panelOpen = ref({ tasks: true, chat: true, notes: true }); const gridColumns = computed(() => { - const cols = [ - panelOpen.value.tasks ? "1fr" : "0px", - panelOpen.value.chat ? "1fr" : "0px", - panelOpen.value.notes ? "1fr" : "0px", - ]; - return cols.join(" "); + return [ + panelOpen.value.tasks ? "minmax(0, 0.8fr)" : "0px", + panelOpen.value.chat ? "minmax(0, 1.1fr)" : "0px", + panelOpen.value.notes ? "minmax(0, 1.1fr)" : "0px", + ].join(" "); }); // SSE watcher @@ -124,6 +124,11 @@ function togglePanel(panel: keyof typeof panelOpen.value) { localStorage.setItem(_panelKey(projectId.value), JSON.stringify(panelOpen.value)); } +function prefill(text: string) { + messageInput.value = text; + nextTick(() => inputEl.value?.focus()); +} + async function sendMessage() { const content = messageInput.value.trim(); if (!content) return; @@ -230,7 +235,7 @@ onUnmounted(async () => { ← {{ project?.title ?? "Project" }} - Workspace + {{ project?.goal ?? '' }}
-

- Ask the agent to create notes or tasks for this project. -

+

What would you like to work on?

+
+ + + +
+
@@ -428,10 +438,13 @@ onUnmounted(async () => { } .ws-title { - font-weight: 600; - font-size: 0.9rem; + font-size: 0.78rem; color: var(--color-text-muted); flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-style: italic; } .ws-panel-toggles { @@ -501,12 +514,37 @@ onUnmounted(async () => { gap: 0.75rem; } -.empty-chat-msg { - color: var(--color-text-muted); - font-size: 0.875rem; +.empty-chat-prompt { text-align: center; padding: 2rem 1rem; } +.empty-hint { + margin: 0 0 1rem; + color: var(--color-text-secondary); + font-size: 0.9rem; +} +.quick-chips { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: center; +} +.quick-chip { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 20px; + padding: 0.4rem 0.85rem; + font-size: 0.82rem; + color: var(--color-text-secondary); + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; + font-family: inherit; +} +.quick-chip:hover { + border-color: var(--color-primary); + color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 5%, var(--color-surface)); +} .chat-input-area { display: flex;