Add multi-user auth, background generation, and chat UX improvements
Phase 5: Multi-user authentication with session cookies, bcrypt passwords, first-user-is-admin pattern, per-user data isolation, backup/restore, Docker Swarm production stack with secrets and network isolation. Phase 5.1: Chat UX improvements: - Background generation architecture (GenerationBuffer + asyncio task) with SSE fan-out, reconnect support, and periodic DB flushes - LLM-generated conversation titles (first exchange + every 10th message) - Stop generation button with cancel_event and partial content preservation - Relative timestamps in sidebar (5m ago, 3h ago, then dates) - Empty chat auto-cleanup on navigation away - Save-as-note uses LLM for title generation, tags notes with "chat" - Summarize-as-note also tags with "chat" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from "@/api/client";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import type { Note, NoteListResponse } from "@/types/note";
|
||||
|
||||
export const useNotesStore = defineStore("notes", () => {
|
||||
@@ -36,6 +37,9 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
);
|
||||
notes.value = data.notes;
|
||||
total.value = data.total;
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to load notes", "error");
|
||||
throw e;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -62,6 +66,9 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
currentNote.value = await apiGet<Note>(`/api/notes/${id}`);
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to load note", "error");
|
||||
throw e;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -71,26 +78,40 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
title: string;
|
||||
body: string;
|
||||
}): Promise<Note> {
|
||||
const note = await apiPost<Note>("/api/notes", data);
|
||||
return note;
|
||||
try {
|
||||
return await apiPost<Note>("/api/notes", data);
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to create note", "error");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateNote(
|
||||
id: number,
|
||||
data: Partial<Pick<Note, "title" | "body">>
|
||||
): Promise<Note> {
|
||||
const note = await apiPut<Note>(`/api/notes/${id}`, data);
|
||||
if (currentNote.value?.id === id) {
|
||||
currentNote.value = note;
|
||||
try {
|
||||
const note = await apiPut<Note>(`/api/notes/${id}`, data);
|
||||
if (currentNote.value?.id === id) {
|
||||
currentNote.value = note;
|
||||
}
|
||||
return note;
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to update note", "error");
|
||||
throw e;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
async function deleteNote(id: number) {
|
||||
await apiDelete(`/api/notes/${id}`);
|
||||
notes.value = notes.value.filter((n) => n.id !== id);
|
||||
if (currentNote.value?.id === id) {
|
||||
currentNote.value = null;
|
||||
try {
|
||||
await apiDelete(`/api/notes/${id}`);
|
||||
notes.value = notes.value.filter((n) => n.id !== id);
|
||||
if (currentNote.value?.id === id) {
|
||||
currentNote.value = null;
|
||||
}
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to delete note", "error");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,26 +164,35 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
}
|
||||
|
||||
async function convertToTask(noteId: number): Promise<Note> {
|
||||
const note = await apiPost<Note>(
|
||||
`/api/notes/${noteId}/convert-to-task`,
|
||||
{}
|
||||
);
|
||||
// Update local state — the note is now a task
|
||||
if (currentNote.value?.id === noteId) {
|
||||
currentNote.value = note;
|
||||
try {
|
||||
const note = await apiPost<Note>(
|
||||
`/api/notes/${noteId}/convert-to-task`,
|
||||
{}
|
||||
);
|
||||
if (currentNote.value?.id === noteId) {
|
||||
currentNote.value = note;
|
||||
}
|
||||
return note;
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to convert to task", "error");
|
||||
throw e;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
async function convertToNote(noteId: number): Promise<Note> {
|
||||
const note = await apiPost<Note>(
|
||||
`/api/notes/${noteId}/convert-to-note`,
|
||||
{}
|
||||
);
|
||||
if (currentNote.value?.id === noteId) {
|
||||
currentNote.value = note;
|
||||
try {
|
||||
const note = await apiPost<Note>(
|
||||
`/api/notes/${noteId}/convert-to-note`,
|
||||
{}
|
||||
);
|
||||
if (currentNote.value?.id === noteId) {
|
||||
currentNote.value = note;
|
||||
}
|
||||
return note;
|
||||
} catch (e) {
|
||||
useToastStore().show("Failed to convert to note", "error");
|
||||
throw e;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
async function fetchBacklinks(
|
||||
|
||||
Reference in New Issue
Block a user