andy-stack_vaultkeeper-ai/Components/ChatModeSelector.svelte
Andrew Beal 5fc1cfba60 refactor: optimize settings change notifications and cleanup subscriptions
Remove unused dispose method and settings subscription from BaseAIClass. Update SettingsService to track which settings changed and notify subscribers with specific changed keys. Components now only react to relevant setting changes. Export RegisterAiProvider and call it when AI provider settings change to ensure provider is re-registered with new configuration.
2026-05-26 18:51:08 +01:00

229 lines
No EOL
8.2 KiB
Svelte

<script lang="ts">
import { ChatMode, iconForChatMode } from "Enums/ChatMode";
import { Copy } from "Enums/Copy";
import { onDestroy, tick } from "svelte";
import { setIcon } from "obsidian";
import type { SettingsService } from "Services/SettingsService";
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
export let focusInput: () => void;
export let chatModeSelectionAreaActive: boolean;
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
const settingsSubscription: object = settingsService.subscribeToSettingsChanged(changed => {
if (changed.includes("chatMode")) {
currentChatMode = settingsService.settings.chatMode;
}
});
onDestroy(() => settingsService.unsubscribe(settingsSubscription));
let height = 0;
let ChatModeSelectionContentDiv: HTMLDivElement;
let chatModeSelectionContainer: HTMLDivElement;
let currentChatMode: ChatMode = settingsService.settings.chatMode;
let selectedChatMode: ChatMode = ChatMode.ReadOnly;
let iconElements: (HTMLDivElement | null)[] = [null, null, null];
let indexedModes: ChatMode[] = [ChatMode.ReadOnly, ChatMode.Edit, ChatMode.Planning];
$: if (iconElements.some(element => element !== null)) {
iconElements.forEach((element, index) => {
if (element) {
setIcon(element, iconForChatMode(indexedModes[index]));
}
});
}
$: chatModeSelectionAreaActive, updateHeight();
$: if (chatModeSelectionAreaActive) {
selectedChatMode = ChatMode.ReadOnly;
setTimeout(() => {
if (chatModeSelectionContainer && chatModeSelectionAreaActive) {
document.addEventListener("click", handleClickOutside);
updateHeight();
if (ChatModeSelectionContentDiv) {
ChatModeSelectionContentDiv.focus();
}
}
}, 10);
} else {
document.removeEventListener("click", handleClickOutside);
}
function updateHeight() {
tick().then(() => {
if (ChatModeSelectionContentDiv) {
height = ChatModeSelectionContentDiv.scrollHeight;
} else {
height = 0;
}
});
}
function handleClickOutside(event: MouseEvent) {
if (chatModeSelectionContainer && !chatModeSelectionContainer.contains(event.target as Node)) {
chatModeSelectionAreaActive = false;
}
}
async function handleChatModeSelect(e?: MouseEvent) {
currentChatMode = selectedChatMode;
chatModeSelectionAreaActive = false;
await settingsService.updateSettings(settings => {
settings.chatMode = currentChatMode
});
e?.preventDefault();
focusInput();
}
async function handleKeydown(e: KeyboardEvent) {
if (!chatModeSelectionAreaActive) {
return;
}
e.preventDefault();
if (e.key.startsWith("Arrow")) {
const index = indexedModes.indexOf(selectedChatMode);
if (e.key === "ArrowUp") {
selectedChatMode = index === 0 ? indexedModes[2] : indexedModes[index - 1];
}
if (e.key === "ArrowDown") {
selectedChatMode = index >= 2 ? indexedModes[0] : indexedModes[index + 1];
}
return;
}
if (e.key === "Enter") {
handleChatModeSelect();
}
if (e.key === "Escape") {
chatModeSelectionAreaActive = false;
return;
}
}
</script>
<div id="chat-mode-selection" style:height="{height}px" bind:this={chatModeSelectionContainer}>
{#if chatModeSelectionAreaActive}
<div
id="chat-mode-selection-inner-container"
bind:this={ChatModeSelectionContentDiv}
role="listbox"
tabindex="0"
on:keydown={handleKeydown}>
<div class="chat-mode-selection-container"
role="option"
tabindex="-1"
aria-selected={selectedChatMode === ChatMode.ReadOnly}
class:current-chat-mode={currentChatMode === ChatMode.ReadOnly}
style:background-color={selectedChatMode === ChatMode.ReadOnly ? "var(--interactive-accent)" : "transparent"}
on:mouseenter={() => selectedChatMode = ChatMode.ReadOnly}
on:mousedown={handleChatModeSelect}
on:keydown={() => {}}>
<div class="chat-mode-selection-icon" bind:this={iconElements[indexedModes.indexOf(ChatMode.ReadOnly)]}></div>
<div class="chat-mode-selection-title">{Copy.ChatModeReadOnlyTitle}</div>
<div class="chat-mode-selection-subtitle">{Copy.ChatModeReadOnlyDesc}</div>
</div>
<div class="chat-mode-selection-container"
role="option"
tabindex="-1"
aria-selected={selectedChatMode === ChatMode.Edit}
class:current-chat-mode={currentChatMode === ChatMode.Edit}
style:background-color={selectedChatMode === ChatMode.Edit ? "var(--interactive-accent)" : "transparent"}
on:mouseenter={() => selectedChatMode = ChatMode.Edit}
on:mousedown={handleChatModeSelect}
on:keydown={() => {}}>
<div class="chat-mode-selection-icon" bind:this={iconElements[indexedModes.indexOf(ChatMode.Edit)]}></div>
<div class="chat-mode-selection-title">{Copy.ChatModeEditTitle}</div>
<div class="chat-mode-selection-subtitle">{Copy.ChatModeEditDesc}</div>
</div>
<div class="chat-mode-selection-container"
role="option"
tabindex="-1"
aria-selected={selectedChatMode === ChatMode.Planning}
class:current-chat-mode={currentChatMode === ChatMode.Planning}
style:background-color={selectedChatMode === ChatMode.Planning ? "var(--interactive-accent)" : "transparent"}
on:mouseenter={() => selectedChatMode = ChatMode.Planning}
on:mousedown={handleChatModeSelect}
on:keydown={() => {}}>
<div class="chat-mode-selection-icon" bind:this={iconElements[indexedModes.indexOf(ChatMode.Planning)]}></div>
<div class="chat-mode-selection-title">{Copy.ChatModePlanningTitle}</div>
<div class="chat-mode-selection-subtitle">{Copy.ChatModePlanningDesc}</div>
</div>
</div>
{/if}
</div>
<style>
#chat-mode-selection {
max-height: 15em;
transition: height 0.2s ease-out;
overflow: auto;
scroll-behavior: smooth;
}
#chat-mode-selection::-webkit-scrollbar {
display: none;
}
#chat-mode-selection-inner-container {
display: flex;
flex-direction: column;
gap: var(--size-2-2);
}
.chat-mode-selection-container {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: auto 1fr;
border-style: solid;
border-radius: var(--size-2-2);
border-color: var(--background-primary-alt);
border-width: 1px;
padding: var(--size-2-2) var(--size-4-2);
cursor: pointer;
}
.chat-mode-selection-icon {
grid-row: 1 / 3;
grid-column: 1;
display: flex;
align-items: center;
justify-content: center;
padding-right: var(--size-4-2);
pointer-events: none;
}
.chat-mode-selection-container.current-chat-mode {
box-shadow: inset 0px 0px 4px 1px var(--color-accent);
border-color: var(--color-accent);
}
.chat-mode-selection-title {
grid-row: 1;
grid-column: 2;
font-family: var(--font-interface-theme);
}
.chat-mode-selection-subtitle {
grid-row: 2;
grid-column: 2;
font-family: var(--font-interface-theme);
font-size: var(--font-smallest);
color: var(--text-muted);
}
:global(.is-mobile) .chat-mode-selection-container[aria-selected="true"] {
background-color: transparent !important;
}
</style>