Add settings page, model management, and chat UX improvements

- Settings infrastructure: key-value settings table, GET/PUT API, Pinia store
- Configurable assistant name (default "Fable") in settings and LLM system prompt
- Model catalog with 18 models in 3 categories (General Purpose, Coding,
  Uncensored / Creative Writing) with download/select/remove functionality
- Move Ollama status indicator from chat views to global nav bar
- Chat bubble layout: user messages right-aligned, assistant left-aligned
- Floating dark input bar with auto-focus and circular send button
- Fix HTML entity rendering (' apostrophe issue in marked/DOMPurify pipeline)
- Fix new chat button navigation (fetchConversation before router.push)
- Recent chats section on home page with "New Chat" button
- Update summary.md with Phase 4.5 changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 21:32:02 -05:00
parent 834fd80640
commit 38b1ac933e
20 changed files with 1257 additions and 236 deletions
+77
View File
@@ -1,15 +1,20 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { apiGet } from "@/api/client";
import type { Note, NoteListResponse } from "@/types/note";
import type { Task, TaskListResponse } from "@/types/task";
import type { Conversation } from "@/types/chat";
import NoteCard from "@/components/NoteCard.vue";
import TaskCard from "@/components/TaskCard.vue";
import type { TaskStatus } from "@/types/task";
import { useTasksStore } from "@/stores/tasks";
import { useChatStore } from "@/stores/chat";
const router = useRouter();
const recentNotes = ref<Note[]>([]);
const recentTasks = ref<Task[]>([]);
const recentChats = ref<Conversation[]>([]);
const loading = ref(true);
const tasksStore = useTasksStore();
@@ -32,6 +37,15 @@ onMounted(async () => {
console.error("Failed to load recent tasks:", e);
}
try {
const chatData = await apiGet<{ conversations: Conversation[]; total: number }>(
"/api/chat/conversations?limit=3&offset=0"
);
recentChats.value = chatData.conversations;
} catch (e) {
console.error("Failed to load recent chats:", e);
}
loading.value = false;
});
@@ -43,6 +57,13 @@ function onStatusToggle(id: number, status: TaskStatus) {
}
});
}
async function newChat() {
const chatStore = useChatStore();
const conv = await chatStore.createConversation();
await chatStore.fetchConversation(conv.id);
router.push(`/chat/${conv.id}`);
}
</script>
<template>
@@ -52,6 +73,28 @@ function onStatusToggle(id: number, status: TaskStatus) {
<p v-if="loading" class="loading">Loading...</p>
<template v-else>
<section class="section">
<div class="section-header">
<h2>Recent Chats</h2>
<router-link to="/chat" class="see-all">See all</router-link>
</div>
<div v-if="recentChats.length" class="cards">
<router-link
v-for="chat in recentChats"
:key="chat.id"
:to="`/chat/${chat.id}`"
class="chat-card"
>
<span class="chat-card-title">{{ chat.title || "Untitled" }}</span>
<span class="chat-card-meta">{{ chat.message_count }} messages</span>
</router-link>
</div>
<div v-else class="empty-state">
<p class="empty-text">No chats yet.</p>
</div>
<button class="btn-cta btn-new-chat" @click="newChat">+ New Chat</button>
</section>
<section class="section">
<div class="section-header">
<h2>Recent Notes</h2>
@@ -130,6 +173,38 @@ function onStatusToggle(id: number, status: TaskStatus) {
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: 8px;
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;
}
.btn-new-chat {
margin-top: 0.75rem;
}
.empty-state {
text-align: center;
padding: 1.5rem 0;
@@ -143,8 +218,10 @@ function onStatusToggle(id: number, status: TaskStatus) {
padding: 0.4rem 1rem;
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 0.9rem;
cursor: pointer;
}
</style>