mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
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:
parent
08a6bde961
commit
2fc9edc194
7 changed files with 73 additions and 39 deletions
|
|
@ -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>
|
||||
|
|
@ -169,6 +169,7 @@
|
|||
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
|
||||
conversation = loadedConversation;
|
||||
conversationService.setCurrentConversationPath(filePath);
|
||||
chatService.onNameChanged?.(loadedConversation.title);
|
||||
conversationStore.clearLoadFlag();
|
||||
scrollToBottom();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
23
styles.css
23
styles.css
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue