mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Restructure system prompt to enforce mandatory complexity evaluation before action. Replace verbose multi-step planning framework with concise gate-based decision model. Add UI for execution plan visibility. Improve planning/execution separation by blocking execution tools during planning phase. Remove cancellation indicator component. Fix conversation deletion bug preventing saves after delete. Strengthen type safety for AIFunction names. Simplify function summary format for planning agent. Update test mocks for new callback signatures.
228 lines
No EOL
6.9 KiB
Svelte
228 lines
No EOL
6.9 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";
|
|
import type { Attachment } from "Conversations/Attachment";
|
|
import ChatPlanArea from "./ChatPlanArea.svelte";
|
|
import type { ExecutionPlanStore } from "Stores/ExecutionPlanStore";
|
|
|
|
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
|
const executionPlanStore: ExecutionPlanStore = Resolve<ExecutionPlanStore>(Services.ExecutionPlanStore);
|
|
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 hasNoApiKey = false;
|
|
let isSubmitting = false;
|
|
let busyPlanning = false;
|
|
let editModeActive = false;
|
|
let currentStreamingMessageId: string | null = null;
|
|
|
|
let conversation: Conversation = new Conversation();
|
|
let attachments: Attachment[] = [];
|
|
|
|
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, attachments, {
|
|
onSubmit: () => {
|
|
isSubmitting = true;
|
|
attachments = [];
|
|
},
|
|
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;
|
|
}
|
|
},
|
|
onPlanningStarted: () => {
|
|
busyPlanning = true;
|
|
},
|
|
onPlanningFinished: () => {
|
|
busyPlanning = false;
|
|
},
|
|
onPlanUpdate: (executionPlan) => {
|
|
executionPlanStore.setPlan(executionPlan);
|
|
},
|
|
onPlanStepUpdate: () => {
|
|
executionPlanStore.updatePlan();
|
|
},
|
|
onPlanComplete: () => {
|
|
executionPlanStore.clearPlan();
|
|
},
|
|
onComplete: async () => {
|
|
isSubmitting = false;
|
|
busyPlanning = false;
|
|
currentThought = null;
|
|
executionPlanStore.clearPlan();
|
|
abortService.reset();
|
|
tick().then(() => {
|
|
chatArea.updateChatAreaLayout("smooth", 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">
|
|
<ChatPlanArea executionPlanState={executionPlanStore.executionPlanState} {editModeActive} {busyPlanning}/>
|
|
|
|
<div id="chat-container">
|
|
<ChatArea messages={conversation.contents} bind:this={chatArea} bind:currentThought bind:isSubmitting bind:chatContainer
|
|
currentStreamingMessageId={currentStreamingMessageId} editModeActive={editModeActive}/>
|
|
</div>
|
|
|
|
<ChatInput
|
|
bind:this={chatInput}
|
|
bind:attachments={attachments}
|
|
{hasNoApiKey}
|
|
{isSubmitting}
|
|
{editModeActive}
|
|
onSubmit={handleSubmit}
|
|
onTogglEeditMode={toggleEditMode}
|
|
onStop={handleStop}
|
|
/>
|
|
</main>
|
|
|
|
<style>
|
|
.container {
|
|
display: grid;
|
|
grid-template-rows: auto 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: 2;
|
|
grid-column: 1;
|
|
overflow: hidden;
|
|
}
|
|
</style> |