refactor: move typing-in animation to global styles and add conversation title to top bar

- Extract typing-in animation from ChatArea to global styles.css for reuse
- Add conversation title display in TopBar with fade transition
- Update ChatService to notify title changes via onNameChanged callback
- Fix ConversationNamingService to trigger callback after saving new title
- Clean up unused isAborting flag in ChatService
- Adjust grid layout in TopBar to accommodate title display
This commit is contained in:
Andrew Beal 2025-10-17 19:34:47 +01:00
parent 08a6bde961
commit 2fc9edc194
7 changed files with 73 additions and 39 deletions

View file

@ -11,6 +11,7 @@
import type AIAgentPlugin from "main";
import { Copy } from "Enums/Copy";
import { Selector } from "Enums/Selector";
import { fade } from "svelte/transition";
export let messages: ConversationContent[] = [];
export let currentThought: string | null = null;
@ -409,27 +410,4 @@
transform: translateY(0);
}
}
/* Welcome text animation */
.typing-in {
overflow: hidden;
white-space: nowrap;
animation: reveal-center 1.5s ease-in-out forwards;
max-width: 0;
margin: 0 auto;
padding: 5px;
}
@keyframes reveal-center {
0% {
max-width: 0;
opacity: 0;
filter: blur(1px);
}
100% {
max-width: 100%;
opacity: 1;
filter: blur(0px);
}
}
</style>

View file

@ -169,6 +169,7 @@
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
conversation = loadedConversation;
conversationService.setCurrentConversationPath(filePath);
chatService.onNameChanged?.(loadedConversation.title);
conversationStore.clearLoadFlag();
scrollToBottom();
}

View file

@ -8,25 +8,36 @@
import type { ConversationHistoryModal } from 'Modals/ConversationHistoryModal';
import { openPluginSettings } from 'Helpers/Helpers';
import type { ChatService } from 'Services/ChatService';
import { tick } from 'svelte';
import { fade } from 'svelte/transition';
export let leaf: WorkspaceLeaf;
export let onNewConversation: (() => void) | undefined = undefined;
const plugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
const conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
const conversationFileSystemService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
const chatService: ChatService = Resolve<ChatService>(Services.ChatService);
let conversationTitle: string = ""
chatService.onNameChanged = (name: string) => {
conversationTitle = "";
tick().then(() => conversationTitle = name);
};
function startNewConversation() {
conversationService.resetCurrentConversation();
conversationFileSystemService.resetCurrentConversation();
conversationStore.reset();
onNewConversation?.();
conversationTitle = "";
}
async function deleteCurrentConversation() {
chatService.stop();
await conversationService.deleteCurrentConversation();
await conversationFileSystemService.deleteCurrentConversation();
conversationStore.reset();
onNewConversation?.();
conversationTitle = "";
}
function openConversationHistory() {
@ -89,7 +100,7 @@
on:click={() => openConversationHistory()}
aria-label="Conversation History"
></button>
<div id="conversation-divider" class="top-bar-divider"></div>
<div id="conversation-divider-1" class="top-bar-divider"></div>
<button
bind:this={settingsButton}
id="settings-button"
@ -97,6 +108,10 @@
on:click={openSettings}
aria-label="AI Agent Settings"
></button>
{#if conversationTitle !== ""}
<div id="conversation-divider-2" class="top-bar-divider" out:fade></div>
<div id="conversation-title" class="typing-in" out:fade>{conversationTitle}</div>
{/if}
<button
bind:this={closeButton}
id="close-button"
@ -123,7 +138,7 @@
grid-column: 2;
display: grid;
grid-template-rows: auto;
grid-template-columns: var(--size-4-2) auto auto auto auto auto 1fr auto var(--size-4-2);
grid-template-columns: var(--size-4-2) auto auto auto auto auto auto 1fr 0.1fr auto var(--size-4-2);
background-color: var(--color-base-30);
border-radius: var(--radius-m);
}
@ -150,7 +165,7 @@
grid-column: 4;
}
#conversation-divider {
#conversation-divider-1 {
grid-row: 1;
grid-column: 5;
}
@ -160,8 +175,25 @@
grid-column: 6;
}
#close-button {
#conversation-divider-2 {
grid-row: 1;
grid-column: 7;
}
#conversation-title {
grid-row: 1;
grid-column: 8;
display: inline-block;
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
color: var(--text-muted);
}
#close-button {
grid-row: 1;
grid-column: 10;
}
</style>

View file

@ -181,7 +181,7 @@
.history-list-modal-date {
grid-row: 1;
grid-column: 1;
margin: 0px var(--size-2-3) 0px var(--size-4-3)
margin-left: var(--size-4-3)
}
.history-list-modal-separator {

View file

@ -22,11 +22,9 @@ export class ChatService {
private aiFunctionService: AIFunctionService;
private namingService: ConversationNamingService;
private abortController: AbortController | null = null;
private isAborting: boolean = false;
private semaphore: Semaphore;
private semaphoreHeld: boolean = false;
private abortController: AbortController | null = null;
constructor() {
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
@ -35,6 +33,8 @@ export class ChatService {
this.semaphore = new Semaphore(1, false);
}
public onNameChanged: ((name: string) => void) | undefined = undefined;
resolveAIProvider() {
this.ai = Resolve<IAIClass>(Services.IAIClass);
}
@ -57,9 +57,9 @@ export class ChatService {
conversation.contents = [...conversation.contents, new ConversationContent(Role.User, userRequest)];
await this.conversationService.saveConversation(conversation);
// Request conversation name on first user message (fire-and-forget)
if (conversation.contents.length === 1) {
this.namingService.requestName(conversation, userRequest, this.abortController);
this.onNameChanged?.(conversation.title); // on change for initial conversation name
this.namingService.requestName(conversation, userRequest, this.onNameChanged, this.abortController);
}
// Process AI responses and function calls
@ -88,7 +88,6 @@ export class ChatService {
} finally {
callbacks.onThoughtUpdate(null);
this.abortController = null;
this.isAborting = false;
if (this.semaphoreHeld) {
this.semaphoreHeld = false;
this.semaphore.release();
@ -98,7 +97,6 @@ export class ChatService {
}
stop(): void {
this.isAborting = true;
if (this.abortController) {
this.abortController.abort();
this.abortController = null;

View file

@ -16,7 +16,7 @@ export class ConversationNamingService {
this.namingProvider = Resolve<IConversationNamingService>(Services.IConversationNamingService);
}
public async requestName(conversation: Conversation, userPrompt: string, abortController: AbortController): Promise<void> {
public async requestName(conversation: Conversation, userPrompt: string, onNameChanged: ((name: string) => void) | undefined, abortController: AbortController): Promise<void> {
if (!this.namingProvider) {
return;
}
@ -38,6 +38,8 @@ export class ConversationNamingService {
await this.conversationService.updateConversationTitle(conversationPath, validatedName);
conversation.title = validatedName;
await this.conversationService.saveConversation(conversation);
onNameChanged?.(conversation.title);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return;

View file

@ -66,6 +66,29 @@
/* CSS Variables for Common Components */
/* ============================== */
/* Typing-in animation (used across components) */
.typing-in {
overflow: hidden;
white-space: nowrap;
animation: reveal-center var(--typing-in-duration, 1.5s) ease-in-out forwards;
max-width: 0;
margin: 0 auto;
padding: 5px;
}
@keyframes reveal-center {
0% {
max-width: 0;
opacity: 0;
filter: blur(1px);
}
100% {
max-width: 100%;
opacity: 1;
filter: blur(0px);
}
}
.conversation-history-modal {
padding: 0px;
overflow: hidden;