mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
The token services weren't providing much value and have become more maintenance than they are worth given the addition of binary conversation data.
204 lines
No EOL
6 KiB
Svelte
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> |