2025-09-16 16:14:37 +00:00
|
|
|
<script lang="ts">
|
2025-09-26 19:48:05 +00:00
|
|
|
import { Resolve } from "Services/DependencyService";
|
|
|
|
|
import { Services } from "Services/Services";
|
2025-09-16 16:14:37 +00:00
|
|
|
import ChatArea from "./ChatArea.svelte";
|
2025-10-26 09:15:11 +00:00
|
|
|
import ChatInput from "./ChatInput.svelte";
|
2025-10-08 21:06:25 +00:00
|
|
|
import { tick, onMount } from "svelte";
|
2025-10-29 19:35:19 +00:00
|
|
|
import { conversationStore } from "../Stores/ConversationStore";
|
2025-10-04 12:10:22 +00:00
|
|
|
import { Conversation } from "Conversations/Conversation";
|
2025-11-10 08:03:27 +00:00
|
|
|
import type VaultkeeperAIPlugin from "main";
|
2025-10-08 21:06:25 +00:00
|
|
|
import { openPluginSettings } from "Helpers/Helpers";
|
|
|
|
|
import { Selector } from "Enums/Selector";
|
|
|
|
|
import type { WorkSpaceService } from "Services/WorkSpaceService";
|
2025-10-12 17:55:39 +00:00
|
|
|
import type { ChatService } from "Services/ChatService";
|
|
|
|
|
import type { ConversationFileSystemService } from "Services/ConversationFileSystemService";
|
2025-11-03 17:14:32 +00:00
|
|
|
import type { SettingsService } from "Services/SettingsService";
|
2025-12-10 21:27:58 +00:00
|
|
|
import { Copy } from "Enums/Copy";
|
|
|
|
|
import { AbortService } from "Services/AbortService";
|
2025-12-22 20:02:02 +00:00
|
|
|
import type { Attachment } from "Conversations/Attachment";
|
2025-10-08 21:06:25 +00:00
|
|
|
|
2025-11-10 08:03:27 +00:00
|
|
|
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
2025-11-03 17:14:32 +00:00
|
|
|
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);
|
2025-12-10 21:27:58 +00:00
|
|
|
const abortService: AbortService = Resolve<AbortService>(Services.AbortService);
|
2025-09-16 16:14:37 +00:00
|
|
|
|
2025-09-28 16:40:05 +00:00
|
|
|
let chatContainer: HTMLDivElement;
|
2025-10-11 23:33:04 +00:00
|
|
|
let chatArea: ChatArea;
|
2025-10-26 09:15:11 +00:00
|
|
|
let chatInput: ChatInput;
|
2025-09-16 16:14:37 +00:00
|
|
|
|
2025-12-04 23:04:20 +00:00
|
|
|
let cancelling = false;
|
2025-10-08 21:06:25 +00:00
|
|
|
let hasNoApiKey = false;
|
2025-09-16 16:14:37 +00:00
|
|
|
let isSubmitting = false;
|
2025-10-13 18:12:06 +00:00
|
|
|
let editModeActive = false;
|
2025-10-12 12:07:54 +00:00
|
|
|
let currentStreamingMessageId: string | null = null;
|
2025-09-29 10:09:22 +00:00
|
|
|
|
2025-10-24 16:25:19 +00:00
|
|
|
let conversation: Conversation = new Conversation();
|
2025-12-22 20:02:02 +00:00
|
|
|
let attachments: Attachment[] = [];
|
2025-09-16 16:14:37 +00:00
|
|
|
|
2025-10-07 21:27:51 +00:00
|
|
|
let currentThought: string | null = null;
|
|
|
|
|
|
2025-10-09 20:34:40 +00:00
|
|
|
export function focusInput() {
|
2025-10-26 09:15:11 +00:00
|
|
|
chatInput?.focusInput();
|
2025-10-09 20:34:40 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 16:25:19 +00:00
|
|
|
export function resetChatArea() {
|
|
|
|
|
chatArea.resetChatArea();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 21:06:25 +00:00
|
|
|
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 {
|
2025-11-03 17:14:32 +00:00
|
|
|
hasNoApiKey = settingsService.getApiKeyForCurrentModel().trim() == "";
|
2025-10-08 21:06:25 +00:00
|
|
|
if (hasNoApiKey) {
|
|
|
|
|
openPluginSettings(plugin);
|
|
|
|
|
}
|
|
|
|
|
return hasNoApiKey;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 19:06:47 +00:00
|
|
|
function toggleEditMode() {
|
|
|
|
|
editModeActive = !editModeActive;
|
|
|
|
|
focusInput();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 12:33:25 +00:00
|
|
|
function handleStop() {
|
2025-10-12 17:55:39 +00:00
|
|
|
chatService.stop();
|
2025-10-12 12:33:25 +00:00
|
|
|
currentThought = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-29 19:35:19 +00:00
|
|
|
async function handleSubmit(userRequest: string, formattedRequest: string) {
|
2025-10-12 17:55:39 +00:00
|
|
|
if (handleNoApiKey()) {
|
2025-09-26 19:48:05 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 17:55:39 +00:00
|
|
|
const currentRequest = userRequest;
|
|
|
|
|
|
2025-12-22 20:02:02 +00:00
|
|
|
await chatService.submit(conversation, editModeActive, currentRequest, formattedRequest, attachments, {
|
2025-10-24 16:25:19 +00:00
|
|
|
onSubmit: () => {
|
|
|
|
|
isSubmitting = true;
|
2025-12-22 20:02:02 +00:00
|
|
|
attachments = [];
|
2025-10-24 16:25:19 +00:00
|
|
|
},
|
2025-10-20 07:30:08 +00:00
|
|
|
onStreamingUpdate: (streamingId) => {
|
|
|
|
|
conversation = conversation;
|
2025-10-12 17:55:39 +00:00
|
|
|
currentStreamingMessageId = streamingId;
|
2025-12-10 21:27:58 +00:00
|
|
|
chatArea.updateChatAreaLayout("smooth");
|
2025-10-12 17:55:39 +00:00
|
|
|
},
|
|
|
|
|
onThoughtUpdate: (thought) => {
|
2025-12-10 21:27:58 +00:00
|
|
|
if (thought !== Copy.AIThoughtMessage) {
|
|
|
|
|
currentThought = thought;
|
|
|
|
|
} else if (currentThought !== null) {
|
|
|
|
|
// we are in-between thoughts so use generic copy
|
|
|
|
|
currentThought = thought;
|
|
|
|
|
}
|
2025-10-12 17:55:39 +00:00
|
|
|
},
|
2025-12-04 23:04:20 +00:00
|
|
|
onComplete: async () => {
|
|
|
|
|
cancelling = false;
|
2025-10-12 17:55:39 +00:00
|
|
|
isSubmitting = false;
|
2025-12-10 21:27:58 +00:00
|
|
|
abortService.reset();
|
2025-12-11 08:42:09 +00:00
|
|
|
chatArea.updateChatAreaLayout("smooth", true);
|
2025-12-04 23:04:20 +00:00
|
|
|
},
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
cancelling = true;
|
2025-10-06 22:01:20 +00:00
|
|
|
}
|
2025-10-12 17:55:39 +00:00
|
|
|
});
|
2025-10-06 22:01:20 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 21:14:30 +00:00
|
|
|
$: if ($conversationStore.shouldReset) {
|
2025-10-04 12:10:22 +00:00
|
|
|
conversation = new Conversation();
|
2025-12-04 23:04:20 +00:00
|
|
|
|
|
|
|
|
isSubmitting = false;
|
|
|
|
|
currentStreamingMessageId = null;
|
|
|
|
|
currentThought = null;
|
|
|
|
|
|
2025-10-03 21:14:30 +00:00
|
|
|
conversationStore.clearResetFlag();
|
|
|
|
|
}
|
2025-10-05 13:15:04 +00:00
|
|
|
|
|
|
|
|
$: if ($conversationStore.conversationToLoad) {
|
2025-10-24 16:25:19 +00:00
|
|
|
conversation.contents = [];
|
2025-12-04 23:04:20 +00:00
|
|
|
|
|
|
|
|
isSubmitting = false;
|
|
|
|
|
currentStreamingMessageId = null;
|
|
|
|
|
currentThought = null;
|
|
|
|
|
|
2025-10-24 16:25:19 +00:00
|
|
|
chatArea.resetChatArea();
|
2025-10-24 17:16:38 +00:00
|
|
|
|
2025-10-24 16:25:19 +00:00
|
|
|
tick().then(() => {
|
|
|
|
|
if ($conversationStore.conversationToLoad) {
|
|
|
|
|
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
|
|
|
|
|
conversation = loadedConversation;
|
|
|
|
|
conversationService.setCurrentConversationPath(filePath);
|
|
|
|
|
chatService.onNameChanged?.(loadedConversation.title);
|
|
|
|
|
conversationStore.clearLoadFlag();
|
2025-12-11 08:42:09 +00:00
|
|
|
chatArea.updateChatAreaLayout("instant", true);
|
2025-10-24 16:25:19 +00:00
|
|
|
}
|
|
|
|
|
});
|
2025-10-05 13:15:04 +00:00
|
|
|
}
|
2025-10-13 18:53:00 +00:00
|
|
|
|
|
|
|
|
$: if ($conversationStore.shouldDeactivateEditMode) {
|
|
|
|
|
conversationStore.clearEditModeFlag();
|
2025-10-20 07:30:08 +00:00
|
|
|
editModeActive = false;
|
2025-10-13 18:53:00 +00:00
|
|
|
}
|
2025-09-16 16:14:37 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<main class="container">
|
|
|
|
|
<div id="chat-container">
|
2025-12-04 23:04:20 +00:00
|
|
|
<ChatArea messages={conversation.contents} bind:this={chatArea} bind:currentThought bind:isSubmitting bind:chatContainer bind:cancelling
|
2025-10-13 18:25:29 +00:00
|
|
|
currentStreamingMessageId={currentStreamingMessageId} editModeActive={editModeActive}/>
|
2025-09-16 16:14:37 +00:00
|
|
|
</div>
|
2025-10-13 18:12:06 +00:00
|
|
|
|
2025-10-26 09:15:11 +00:00
|
|
|
<ChatInput
|
|
|
|
|
bind:this={chatInput}
|
2025-12-22 20:02:02 +00:00
|
|
|
bind:attachments={attachments}
|
2025-10-26 09:15:11 +00:00
|
|
|
{hasNoApiKey}
|
|
|
|
|
{isSubmitting}
|
|
|
|
|
{editModeActive}
|
|
|
|
|
onsubmit={handleSubmit}
|
|
|
|
|
ontoggleeditmode={toggleEditMode}
|
|
|
|
|
onstop={handleStop}
|
|
|
|
|
/>
|
2025-09-16 16:14:37 +00:00
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.container {
|
|
|
|
|
display: grid;
|
2025-10-07 21:27:51 +00:00
|
|
|
grid-template-rows: 1fr auto var(--size-2-1);
|
2025-09-16 16:14:37 +00:00
|
|
|
grid-template-columns: 1fr;
|
2025-10-02 08:34:54 +00:00
|
|
|
height: calc(100% - var(--size-4-16));
|
2025-09-16 16:14:37 +00:00
|
|
|
border-radius: var(--radius-m);
|
|
|
|
|
color: var(--font-interface-theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-container {
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
2025-10-20 07:30:08 +00:00
|
|
|
max-width: 1000px;
|
2025-09-28 23:09:10 +00:00
|
|
|
justify-self: center;
|
|
|
|
|
user-select: text;
|
2025-09-16 16:14:37 +00:00
|
|
|
grid-row: 1;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
</style>
|