andy-stack_vaultkeeper-ai/Components/ChatWindow.svelte
Andrew Beal 385271cb75 Remove Token Services.
The token services weren't providing much value and have become more maintenance than they are worth given the addition of binary conversation data.
2025-12-17 10:54:04 +00:00

204 lines
No EOL
6 KiB
Svelte

<script lang="ts">
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import ChatArea from "./ChatArea.svelte";
import ChatInput from "./ChatInput.svelte";
import { tick, onMount } from "svelte";
import { conversationStore } from "../Stores/ConversationStore";
import { Conversation } from "Conversations/Conversation";
import type VaultkeeperAIPlugin from "main";
import { openPluginSettings } from "Helpers/Helpers";
import { Selector } from "Enums/Selector";
import type { WorkSpaceService } from "Services/WorkSpaceService";
import type { ChatService } from "Services/ChatService";
import type { ConversationFileSystemService } from "Services/ConversationFileSystemService";
import type { SettingsService } from "Services/SettingsService";
import { Copy } from "Enums/Copy";
import { AbortService } from "Services/AbortService";
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
const chatService: ChatService = Resolve<ChatService>(Services.ChatService);
const workSpaceService: WorkSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
const conversationService: ConversationFileSystemService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
const abortService: AbortService = Resolve<AbortService>(Services.AbortService);
let chatContainer: HTMLDivElement;
let chatArea: ChatArea;
let chatInput: ChatInput;
let cancelling = false;
let hasNoApiKey = false;
let isSubmitting = false;
let editModeActive = false;
let currentStreamingMessageId: string | null = null;
let conversation: Conversation = new Conversation();
let currentThought: string | null = null;
export function focusInput() {
chatInput?.focusInput();
}
export function resetChatArea() {
chatArea.resetChatArea();
}
onMount(() => {
if (chatContainer) {
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
}
});
async function handleLinkClick(evt: MouseEvent) {
const target = evt.target as HTMLElement;
const link = target.closest(`.${Selector.MarkDownLink}`) as HTMLAnchorElement | null;
if (!link) {
return;
}
const href = link.getAttribute('href');
if (!href || !href.startsWith('#/page/')) {
return;
}
evt.preventDefault();
evt.stopPropagation();
const encodedPath = href.replace('#/page/', '');
const notePath = decodeURIComponent(encodedPath);
await workSpaceService.openNote(notePath);
}
function handleNoApiKey(): boolean {
hasNoApiKey = settingsService.getApiKeyForCurrentModel().trim() == "";
if (hasNoApiKey) {
openPluginSettings(plugin);
}
return hasNoApiKey;
}
function toggleEditMode() {
editModeActive = !editModeActive;
focusInput();
}
function handleStop() {
chatService.stop();
currentThought = null;
}
async function handleSubmit(userRequest: string, formattedRequest: string) {
if (handleNoApiKey()) {
return;
}
const currentRequest = userRequest;
await chatService.submit(conversation, editModeActive, currentRequest, formattedRequest, {
onSubmit: () => {
chatArea.updateChatAreaLayout("smooth");
isSubmitting = true;
},
onStreamingUpdate: (streamingId) => {
conversation = conversation;
currentStreamingMessageId = streamingId;
chatArea.updateChatAreaLayout("smooth");
},
onThoughtUpdate: (thought) => {
if (thought !== Copy.AIThoughtMessage) {
currentThought = thought;
} else if (currentThought !== null) {
// we are in-between thoughts so use generic copy
currentThought = thought;
}
},
onComplete: async () => {
cancelling = false;
isSubmitting = false;
abortService.reset();
chatArea.updateChatAreaLayout("smooth", true);
},
onCancel: () => {
cancelling = true;
}
});
}
$: if ($conversationStore.shouldReset) {
conversation = new Conversation();
isSubmitting = false;
currentStreamingMessageId = null;
currentThought = null;
conversationStore.clearResetFlag();
}
$: if ($conversationStore.conversationToLoad) {
conversation.contents = [];
isSubmitting = false;
currentStreamingMessageId = null;
currentThought = null;
chatArea.resetChatArea();
tick().then(() => {
if ($conversationStore.conversationToLoad) {
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
conversation = loadedConversation;
conversationService.setCurrentConversationPath(filePath);
chatService.onNameChanged?.(loadedConversation.title);
conversationStore.clearLoadFlag();
chatArea.updateChatAreaLayout("instant", true);
}
});
}
$: if ($conversationStore.shouldDeactivateEditMode) {
conversationStore.clearEditModeFlag();
editModeActive = false;
}
</script>
<main class="container">
<div id="chat-container">
<ChatArea messages={conversation.contents} bind:this={chatArea} bind:currentThought bind:isSubmitting bind:chatContainer bind:cancelling
currentStreamingMessageId={currentStreamingMessageId} editModeActive={editModeActive}/>
</div>
<ChatInput
bind:this={chatInput}
{hasNoApiKey}
{isSubmitting}
{editModeActive}
onsubmit={handleSubmit}
ontoggleeditmode={toggleEditMode}
onstop={handleStop}
/>
</main>
<style>
.container {
display: grid;
grid-template-rows: 1fr auto var(--size-2-1);
grid-template-columns: 1fr;
height: calc(100% - var(--size-4-16));
border-radius: var(--radius-m);
color: var(--font-interface-theme);
}
#chat-container {
height: 100%;
width: 100%;
max-width: 1000px;
justify-self: center;
user-select: text;
grid-row: 1;
grid-column: 1;
overflow: hidden;
}
</style>