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,10 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const authStore = useAuthStore();
|
||||
const chatStore = useChatStore();
|
||||
const router = useRouter();
|
||||
const mobileMenuOpen = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleChatPanel: [];
|
||||
@@ -23,13 +29,38 @@ const statusClass = computed(() => {
|
||||
if (chatStore.chatReady) return "status-green";
|
||||
return "status-yellow";
|
||||
});
|
||||
|
||||
function toggleMobileMenu() {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout();
|
||||
mobileMenuOpen.value = false;
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
// Close mobile menu on route change
|
||||
router.afterEach(() => {
|
||||
mobileMenuOpen.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<nav class="nav">
|
||||
<router-link to="/" class="nav-brand">Fabled Assistant</router-link>
|
||||
<div class="nav-links">
|
||||
<router-link to="/" class="nav-brand">
|
||||
<AppLogo />
|
||||
Fabled Assistant
|
||||
</router-link>
|
||||
|
||||
<button class="hamburger" @click="toggleMobileMenu" aria-label="Toggle menu">
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
</button>
|
||||
|
||||
<div class="nav-links" :class="{ open: mobileMenuOpen }">
|
||||
<router-link to="/notes" class="nav-link">Notes</router-link>
|
||||
<router-link to="/tasks" class="nav-link">Tasks</router-link>
|
||||
<router-link to="/chat" class="nav-link">Chat</router-link>
|
||||
@@ -43,6 +74,13 @@ const statusClass = computed(() => {
|
||||
<button class="theme-toggle" @click="toggleTheme" :title="`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`">
|
||||
{{ theme === "dark" ? "\u2600" : "\u263E" }}
|
||||
</button>
|
||||
<div class="user-info">
|
||||
<span class="username">{{ authStore.user?.username }}</span>
|
||||
<span v-if="authStore.isAdmin" class="admin-badge">admin</span>
|
||||
<button class="btn-logout" @click="handleLogout" title="Sign out">
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -60,11 +98,30 @@ const statusClass = computed(() => {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.hamburger {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.hamburger-line {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: var(--color-text);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -139,4 +196,84 @@ const statusClass = computed(() => {
|
||||
.theme-toggle:hover {
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.username {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.admin-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-primary);
|
||||
background: color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.btn-logout {
|
||||
background: none;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.2rem 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.btn-logout:hover {
|
||||
color: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
.nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-direction: column;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.25rem;
|
||||
z-index: 100;
|
||||
}
|
||||
.nav-links.open {
|
||||
display: flex;
|
||||
}
|
||||
.nav-link {
|
||||
padding: 0.5rem 0.75rem;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.status-indicator {
|
||||
margin-left: 0;
|
||||
}
|
||||
.user-info {
|
||||
margin-left: 0;
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
}
|
||||
.btn-chat-panel,
|
||||
.theme-toggle,
|
||||
.btn-logout {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user