mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: replace edit/planning mode toggles with unified chat mode selector
Replace separate editModeActive and planningModeActive boolean flags with a single ChatMode enum (ReadOnly, Edit, Planning). Add ChatModeSelector component for mode switching. Update all components to use chatMode instead of individual flags. Refactor MainAgent and ChatService to accept ChatMode parameter. Remove edit-mode styling variants throughout UI components. Update system prompt to document user reference system. Consolidate input button styling with shared input-button-highlight class.
This commit is contained in:
parent
d3c2a01da9
commit
82e58b52e7
16 changed files with 343 additions and 159 deletions
|
|
@ -22,6 +22,7 @@ import { CreateVaultFolder } from "./Tools/CreateVaultFolder";
|
|||
import { GetWebViewerContent } from "./Tools/GetWebViewerContent";
|
||||
import { DeleteVaultFolder } from "./Tools/DeleteVaultFolder";
|
||||
import { MoveVaultFolder } from "./Tools/MoveVaultFolder";
|
||||
import { ChatMode, chatModeAllowsEdits } from "Enums/ChatMode";
|
||||
|
||||
export abstract class AIToolDefinitions {
|
||||
|
||||
|
|
@ -31,10 +32,10 @@ export abstract class AIToolDefinitions {
|
|||
private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent,
|
||||
WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder, DeleteVaultFolder, MoveVaultFolder];
|
||||
|
||||
public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean, webViewer: boolean): IAIToolDefinition[] {
|
||||
public static agentDefinitions(chatMode: ChatMode, memories: boolean, updateMemories: boolean, webViewer: boolean): IAIToolDefinition[] {
|
||||
this.isGated = false;
|
||||
|
||||
if (planningMode) {
|
||||
if (chatMode === ChatMode.Planning) {
|
||||
return [ExecuteWorkflow];
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ export abstract class AIToolDefinitions {
|
|||
actions = actions.concat([UpdateMemories]);
|
||||
}
|
||||
|
||||
if (destructive) {
|
||||
if (chatModeAllowsEdits(chatMode)) {
|
||||
actions = actions.concat([
|
||||
WriteVaultFile,
|
||||
PatchVaultFile,
|
||||
|
|
@ -110,7 +111,7 @@ export abstract class AIToolDefinitions {
|
|||
}
|
||||
|
||||
public static executionAgentDefinitions(): IAIToolDefinition[] {
|
||||
return [...this.agentDefinitions(true, false, false, false, false), CompleteTask];
|
||||
return [...this.agentDefinitions(ChatMode.Edit, false, false, false), CompleteTask];
|
||||
}
|
||||
|
||||
public static compactSummaryForPlanningAgent(): string {
|
||||
|
|
|
|||
|
|
@ -327,6 +327,19 @@ When searches return or reference images or PDFs:
|
|||
- Extract relevant information to answer the user's query
|
||||
- Reference the source file with [[wiki-links]] as usual
|
||||
|
||||
### User-Provided References
|
||||
|
||||
The chat input supports an autocomplete reference system. When a user types \`@\`, \`/\`, or \`#\`, they can select matching vault items from a dropdown. Selected references are serialized into the message as:
|
||||
|
||||
- \`file:"path/to/note.md"\` — a specific file the user selected with \`@\`
|
||||
- \`folder:"path/to/folder"\` — a specific folder the user selected with \`/\`
|
||||
- \`tag:"#tagname"\` — a specific tag the user selected with \`#\`
|
||||
|
||||
**These references are always valid — the user selected them from live vault data.** You likely do not have to verify their existence.
|
||||
Example: If the user says "create a note in \`folder:"Projects/2025"\`", the folder exists and should be used directly as the destination (does not need to be created first).
|
||||
|
||||
---
|
||||
|
||||
### File Attachments
|
||||
|
||||
**Users can attach files directly to their messages.** When a file is attached, its content is provided inline in the conversation. Attached files are likely NOT present in the vault so use the attached content directly.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
export let isSubmitting: boolean = false;
|
||||
export let chatContainer: HTMLDivElement;
|
||||
export let currentStreamingMessageId: string | null = null;
|
||||
export let editModeActive: boolean = false;
|
||||
|
||||
export function resetChatArea() {
|
||||
autoScroll = true;
|
||||
|
|
@ -291,9 +290,9 @@
|
|||
{/if}
|
||||
{/each}
|
||||
|
||||
<ThoughtIndicator thought={currentThought} {editModeActive} bind:thoughtIndicatorElement={thoughtIndicatorElement}/>
|
||||
<ThoughtIndicator thought={currentThought} bind:thoughtIndicatorElement={thoughtIndicatorElement}/>
|
||||
{#if isSubmitting}
|
||||
<StreamingIndicator editModeActive={editModeActive} bind:streamingIndicatorElement={streamingIndicatorElement}/>
|
||||
<StreamingIndicator bind:streamingIndicatorElement={streamingIndicatorElement}/>
|
||||
{/if}
|
||||
|
||||
<div bind:this={chatAreaPaddingElement} style:user-select=none></div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import type { Writable } from "svelte/store";
|
||||
import type { InputService } from "Services/InputService";
|
||||
import UserInstruction from "./UserInstruction.svelte";
|
||||
import ChatModeSelector from "./ChatModeSelector.svelte";
|
||||
import DiffControls from "./DiffControls.svelte";
|
||||
import type { EventService } from "Services/EventService";
|
||||
import { Event } from "Enums/Event";
|
||||
|
|
@ -22,13 +23,13 @@
|
|||
import { HelpModal } from "Modals/HelpModal";
|
||||
import type { IPrompt } from "AIPrompts/IPrompt";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import { ChatMode, chatModeAllowsEdits, iconForChatMode } from "Enums/ChatMode";
|
||||
|
||||
export let attachments: Attachment[] = [];
|
||||
|
||||
export let hasNoApiKey: boolean;
|
||||
export let isSubmitting: boolean;
|
||||
export let editModeActive: boolean;
|
||||
export let planningModeActive: boolean;
|
||||
export let chatMode: ChatMode;
|
||||
export let onSubmit: (userRequest: string, formattedRequest: string) => void;
|
||||
export let onStop: () => void;
|
||||
|
||||
|
|
@ -47,9 +48,10 @@
|
|||
let userInstructionButton: HTMLButtonElement;
|
||||
let webSearchButton: HTMLButtonElement;
|
||||
let submitButton: HTMLButtonElement;
|
||||
let editModeButton: HTMLButtonElement;
|
||||
let planningModeButton: HTMLButtonElement;
|
||||
let attachmentButton: HTMLButtonElement;
|
||||
let chatModeButton: HTMLButtonElement;
|
||||
|
||||
let chatModeSelectionAreaActive: boolean = false;
|
||||
let userInstructionAreaActive: boolean = false;
|
||||
let userInstructionActive: boolean = true;
|
||||
let stacked: boolean = false;
|
||||
|
|
@ -198,12 +200,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: if (editModeButton) {
|
||||
setIcon(editModeButton, editModeActive ? "pencil" : "pencil-off");
|
||||
$: if (attachmentButton) {
|
||||
setIcon(attachmentButton, "paperclip");
|
||||
}
|
||||
|
||||
$: if (planningModeButton) {
|
||||
setIcon(planningModeButton, planningModeActive ? "list-ordered" : "list-x");
|
||||
$: if (chatModeButton) {
|
||||
setIcon(chatModeButton, iconForChatMode(chatMode));
|
||||
}
|
||||
|
||||
$: inputPlaceholder = (() => {
|
||||
|
|
@ -290,24 +292,8 @@
|
|||
settingsService.saveSettings();
|
||||
}
|
||||
|
||||
function toggleEditMode() {
|
||||
if (planningModeActive) {
|
||||
planningModeActive = false
|
||||
}
|
||||
editModeActive = !editModeActive;
|
||||
focusInput();
|
||||
}
|
||||
|
||||
function togglePlanningMode() {
|
||||
if (planningModeActive) {
|
||||
planningModeActive = false;
|
||||
} else {
|
||||
if (!editModeActive) {
|
||||
toggleEditMode(); // Mandatory for planning mode
|
||||
}
|
||||
planningModeActive = true;
|
||||
}
|
||||
focusInput();
|
||||
function toggleChatModeSelectionArea() {
|
||||
chatModeSelectionAreaActive = !chatModeSelectionAreaActive;
|
||||
}
|
||||
|
||||
async function handleKeydown(e: KeyboardEvent) {
|
||||
|
|
@ -501,9 +487,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div id="input-container" class:edit-mode={editModeActive} class:stacked>
|
||||
<div id="input-container" class:stacked>
|
||||
<div id="input-display-container" style:padding-top={attachments.length > 0 ? "var(--size-4-2)" : 0}>
|
||||
<InputDisplay bind:this={inputDisplay} {editModeActive}/>
|
||||
<InputDisplay bind:this={inputDisplay}/>
|
||||
</div>
|
||||
|
||||
<div id="input-attachments-container" style:padding-top={attachments.length > 0 ? "var(--size-4-2)" : 0}>
|
||||
|
|
@ -522,9 +508,13 @@
|
|||
<UserInstruction focusInput={focusInput} bind:userInstructionAreaActive={userInstructionAreaActive}/>
|
||||
</div>
|
||||
|
||||
<div id="chat-mode-selector-container" style:padding-top={chatModeSelectionAreaActive ? "var(--size-4-2)" : 0}>
|
||||
<ChatModeSelector focusInput={focusInput} bind:chatModeSelectionAreaActive={chatModeSelectionAreaActive} bind:currentChatMode={chatMode} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="user-instruction-button"
|
||||
class:instruction-active={userInstructionActive}
|
||||
class:input-button-highlight={userInstructionActive}
|
||||
bind:this={userInstructionButton}
|
||||
on:click={toggleUserInstructionArea}
|
||||
aria-label={Copy.ButtonUserInstruction}>
|
||||
|
|
@ -532,7 +522,7 @@
|
|||
|
||||
<button
|
||||
id="web-search-button"
|
||||
class:web-search-active={settingsService.settings.enableWebSearch}
|
||||
class:input-button-highlight={settingsService.settings.enableWebSearch}
|
||||
bind:this={webSearchButton}
|
||||
on:click={toggleWebSearch}
|
||||
aria-label={settingsService.settings.enableWebSearch ? Copy.ButtonTurnOffWebSearch : Copy.ButtonTurnOnWebSearch}>
|
||||
|
|
@ -541,7 +531,6 @@
|
|||
<div
|
||||
id="input-field"
|
||||
class:error={hasNoApiKey}
|
||||
class:edit-mode={editModeActive && !hasNoApiKey}
|
||||
bind:this={textareaElement}
|
||||
contenteditable="plaintext-only"
|
||||
on:keydown={handleKeydown}
|
||||
|
|
@ -563,26 +552,25 @@
|
|||
</div>
|
||||
|
||||
<button
|
||||
id="edit-mode-button"
|
||||
class:edit-mode={editModeActive}
|
||||
bind:this={editModeButton}
|
||||
on:click={() => { toggleEditMode() }}
|
||||
id="chat-attachment-button"
|
||||
bind:this={attachmentButton}
|
||||
on:click={() => { }}
|
||||
disabled={isSubmitting}
|
||||
aria-label={editModeActive ? Copy.ButtonTurnOffAgentMode : Copy.ButtonTurnOnAgentMode}>
|
||||
aria-label={"Attachment"}>
|
||||
<!-- Copy.AttachmentLabel -->
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="planning-mode-button"
|
||||
class:planning-mode={planningModeActive}
|
||||
bind:this={planningModeButton}
|
||||
on:click={() => { togglePlanningMode() }}
|
||||
id="chat-mode-button"
|
||||
class:input-button-highlight={chatModeAllowsEdits(chatMode)}
|
||||
bind:this={chatModeButton}
|
||||
on:click={() => { toggleChatModeSelectionArea() }}
|
||||
disabled={isSubmitting}
|
||||
aria-label={planningModeActive ? Copy.ButtonTurnOffPlanningMode : Copy.ButtonTurnOnPlanningMode}>
|
||||
aria-label={Copy.ButtonChangeChatMode}>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="submit-button"
|
||||
class:edit-mode={editModeActive}
|
||||
bind:this={submitButton}
|
||||
on:click={() => {
|
||||
if (inputMode === InputMode.Question) {
|
||||
|
|
@ -609,11 +597,6 @@
|
|||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
#input-container.edit-mode {
|
||||
border-color: var(--alt-interactive-accent);
|
||||
transition: border-color 0.5s ease-out;
|
||||
}
|
||||
|
||||
#input-display-container {
|
||||
grid-row: 1;
|
||||
grid-column: 2 / 13;
|
||||
|
|
@ -639,10 +622,16 @@
|
|||
grid-column: 2 / 13;
|
||||
}
|
||||
|
||||
#chat-mode-selector-container {
|
||||
grid-row: 4;
|
||||
grid-column: 2 / 13;
|
||||
}
|
||||
|
||||
#user-instruction-button {
|
||||
grid-row: 6;
|
||||
grid-column: 2;
|
||||
border-radius: var(--radius-l);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--size-4-2);
|
||||
align-self: end;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
|
@ -651,22 +640,15 @@
|
|||
max-height: 2rem;
|
||||
}
|
||||
|
||||
#user-instruction-button.instruction-active {
|
||||
box-shadow: 0px 0px 2px 1px var(--color-accent);
|
||||
}
|
||||
|
||||
#web-search-button {
|
||||
grid-row: 6;
|
||||
grid-column: 4;
|
||||
border-radius: var(--radius-l);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--size-4-2);
|
||||
align-self: end;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
#web-search-button.web-search-active {
|
||||
box-shadow: 0px 0px 2px 1px var(--color-accent);
|
||||
}
|
||||
|
||||
:global(.is-mobile) #web-search-button {
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
|
@ -704,12 +686,6 @@
|
|||
transition: border-color 0.5s ease-out;
|
||||
}
|
||||
|
||||
#input-field.edit-mode:focus {
|
||||
border-color: var(--alt-interactive-accent);
|
||||
box-shadow: 0px 0px 3px 1px var(--alt-interactive-accent);
|
||||
transition: border-color 0.5s ease-out;
|
||||
}
|
||||
|
||||
#input-field.error,
|
||||
#input-field.error:focus {
|
||||
border-color: var(--color-red);
|
||||
|
|
@ -732,44 +708,42 @@
|
|||
outline: none;
|
||||
}
|
||||
|
||||
#edit-mode-button {
|
||||
#chat-attachment-button {
|
||||
grid-row: 6;
|
||||
grid-column: 8;
|
||||
border-radius: var(--radius-l);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--size-4-2);
|
||||
align-self: end;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
#edit-mode-button.edit-mode {
|
||||
box-shadow: inset 0px 0px 1px 1px var(--alt-interactive-accent);
|
||||
}
|
||||
|
||||
:global(.is-mobile) #edit-mode-button {
|
||||
:global(.is-mobile) #chat-attachment-button {
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
#planning-mode-button {
|
||||
#chat-mode-button {
|
||||
grid-row: 6;
|
||||
grid-column: 10;
|
||||
border-radius: var(--radius-l);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--size-4-2);
|
||||
align-self: end;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
#planning-mode-button.planning-mode {
|
||||
box-shadow: inset 0px 0px 1px 1px var(--alt-interactive-accent);
|
||||
:global(.is-mobile) #chat-mode-button {
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
:global(.is-mobile) #planning-mode-button {
|
||||
max-height: 2rem;
|
||||
.input-button-highlight {
|
||||
box-shadow: 0px 0px 2px 1px var(--color-accent);
|
||||
}
|
||||
|
||||
#submit-button {
|
||||
grid-row: 6;
|
||||
grid-column: 12;
|
||||
border-radius: var(--radius-l);
|
||||
padding-left: var(--size-4-5);
|
||||
padding-right: var(--size-4-5);
|
||||
border-radius: var(--radius-xl);
|
||||
padding-left: var(--size-4-2);
|
||||
padding-right: var(--size-4-2);
|
||||
align-self: end;
|
||||
transition-duration: 0.5s;
|
||||
background-color: var(--interactive-accent);
|
||||
|
|
@ -784,15 +758,6 @@
|
|||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
#submit-button.edit-mode {
|
||||
background-color: var(--alt-interactive-accent);
|
||||
}
|
||||
|
||||
#submit-button.edit-mode:not(:disabled):hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--alt-interactive-accent-hover);
|
||||
}
|
||||
|
||||
/* Stacked layout: input above, buttons below (desktop only, when content wraps) */
|
||||
#input-container.stacked {
|
||||
grid-template-rows: auto auto auto auto var(--size-4-3) 1fr var(--size-4-2) auto var(--size-4-3);
|
||||
|
|
@ -810,11 +775,11 @@
|
|||
grid-row: 8;
|
||||
}
|
||||
|
||||
#input-container.stacked #edit-mode-button {
|
||||
#input-container.stacked #chat-attachment-button {
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
#input-container.stacked #planning-mode-button {
|
||||
#input-container.stacked #chat-mode-button {
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
|
|
@ -832,7 +797,8 @@
|
|||
:global(.is-mobile) #input-attachments-container,
|
||||
:global(.is-mobile) #diff-controls-container,
|
||||
:global(.is-mobile) #input-search-results-container,
|
||||
:global(.is-mobile) #user-instruction-container {
|
||||
:global(.is-mobile) #user-instruction-container,
|
||||
:global(.is-mobile) #chat-mode-selector-container {
|
||||
grid-column: 2 / 11;
|
||||
}
|
||||
|
||||
|
|
@ -851,12 +817,12 @@
|
|||
grid-column: 4;
|
||||
}
|
||||
|
||||
:global(.is-mobile) #edit-mode-button {
|
||||
:global(.is-mobile) #chat-attachment-button {
|
||||
grid-row: 8;
|
||||
grid-column: 6;
|
||||
}
|
||||
|
||||
:global(.is-mobile) #planning-mode-button {
|
||||
:global(.is-mobile) #chat-mode-button {
|
||||
grid-row: 8;
|
||||
grid-column: 8;
|
||||
}
|
||||
|
|
|
|||
210
Components/ChatModeSelector.svelte
Normal file
210
Components/ChatModeSelector.svelte
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<script lang="ts">
|
||||
import { ChatMode, iconForChatMode } from "Enums/ChatMode";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { tick } from "svelte";
|
||||
import { setIcon } from "obsidian";
|
||||
|
||||
export let focusInput: () => void;
|
||||
export let chatModeSelectionAreaActive: boolean;
|
||||
export let currentChatMode: ChatMode;
|
||||
|
||||
let height = 0;
|
||||
|
||||
let ChatModeSelectionContentDiv: HTMLDivElement;
|
||||
let chatModeSelectionContainer: HTMLDivElement;
|
||||
|
||||
let selectedChatMode: number = ChatMode.ReadOnly;
|
||||
|
||||
let iconElements: (HTMLDivElement | null)[] = [null, null, null];
|
||||
|
||||
$: if (iconElements.some(element => element !== null)) {
|
||||
iconElements.forEach((element, index) => {
|
||||
if (element) {
|
||||
setIcon(element, iconForChatMode(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;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChatModeSelect(e?: MouseEvent) {
|
||||
currentChatMode = selectedChatMode;
|
||||
chatModeSelectionAreaActive = false;
|
||||
|
||||
e?.preventDefault();
|
||||
focusInput();
|
||||
}
|
||||
|
||||
async function handleKeydown(e: KeyboardEvent) {
|
||||
if (!chatModeSelectionAreaActive) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key.startsWith("Arrow")) {
|
||||
if (e.key === "ArrowUp") {
|
||||
selectedChatMode = selectedChatMode <= 0 ? 2 : selectedChatMode - 1;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
selectedChatMode = selectedChatMode >= 2 ? 0 : selectedChatMode + 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[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[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[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>
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
import type { Writable } from "svelte/store";
|
||||
|
||||
export let executionPlanState: Writable<IExecutionPlanState>;
|
||||
export let editModeActive = false;
|
||||
export let busyPlanning = false;
|
||||
|
||||
let expanded = false;
|
||||
|
|
@ -116,7 +115,7 @@
|
|||
|
||||
{#if busyPlanning}
|
||||
<div id="chat-planning-in-progress" transition:slide>
|
||||
<Spinner {editModeActive} alternateBackground={true}/>
|
||||
<Spinner alternateBackground={true}/>
|
||||
<span id="chat-planning-in-progress-text">{Copy.PlanningInProgress}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -137,7 +136,7 @@
|
|||
{/if}
|
||||
{#if index === activeStepIndex}
|
||||
<div class="chat-plan-step-icon">
|
||||
<Spinner {editModeActive} alternateBackground={true}/>
|
||||
<Spinner alternateBackground={true}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if index > activeStepIndex}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
|
||||
import { HTMLService } from "Services/HTMLService";
|
||||
import { AITool, fromString } from "Enums/AITool";
|
||||
import { ChatMode } from "Enums/ChatMode";
|
||||
|
||||
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const executionPlanStore: ExecutionPlanStore = Resolve<ExecutionPlanStore>(Services.ExecutionPlanStore);
|
||||
|
|
@ -39,8 +40,7 @@
|
|||
let hasNoApiKey = false;
|
||||
let isSubmitting = false;
|
||||
let busyPlanning = false;
|
||||
let editModeActive = false;
|
||||
let planningModeActive = false;
|
||||
let chatMode: ChatMode = ChatMode.ReadOnly;
|
||||
let currentStreamingMessageId: string | null = null;
|
||||
|
||||
let conversation: Conversation = new Conversation();
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
|
||||
const currentRequest = userRequest;
|
||||
|
||||
await chatService.submit(conversation, editModeActive, planningModeActive, currentRequest, formattedRequest, attachments, {
|
||||
await chatService.submit(conversation, chatMode, currentRequest, formattedRequest, attachments, {
|
||||
onSubmit: () => {
|
||||
isSubmitting = true;
|
||||
attachments = [];
|
||||
|
|
@ -207,24 +207,22 @@
|
|||
|
||||
$: if ($conversationStore.shouldDeactivateEditMode) {
|
||||
conversationStore.clearEditModeFlag();
|
||||
planningModeActive = false;
|
||||
editModeActive = false;
|
||||
chatMode = ChatMode.ReadOnly;
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container">
|
||||
<ChatPlanArea executionPlanState={executionPlanStore.executionPlanState} {editModeActive} {busyPlanning}/>
|
||||
<ChatPlanArea executionPlanState={executionPlanStore.executionPlanState} {busyPlanning}/>
|
||||
|
||||
<div id="chat-container">
|
||||
<ChatArea messages={conversation.contents} bind:this={chatArea} bind:currentThought bind:isSubmitting bind:chatContainer
|
||||
currentStreamingMessageId={currentStreamingMessageId} editModeActive={editModeActive}/>
|
||||
currentStreamingMessageId={currentStreamingMessageId}/>
|
||||
</div>
|
||||
|
||||
<ChatInput
|
||||
bind:this={chatInput}
|
||||
bind:attachments
|
||||
bind:editModeActive
|
||||
bind:planningModeActive
|
||||
bind:chatMode={chatMode}
|
||||
{hasNoApiKey}
|
||||
{isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
export let editModeActive = false;
|
||||
|
||||
let displayItem: HTMLElement | undefined;
|
||||
let contentDiv: HTMLDivElement;
|
||||
|
||||
|
|
@ -22,7 +20,7 @@
|
|||
|
||||
{#if displayItem}
|
||||
<div id="input-display-wrapper" transition:slide>
|
||||
<div id="input-display-container" class:edit-mode={editModeActive} bind:this={contentDiv}>
|
||||
<div id="input-display-container" bind:this={contentDiv}>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -56,12 +54,6 @@
|
|||
max-height: 15vh;
|
||||
}
|
||||
|
||||
#input-display-container.edit-mode {
|
||||
border-color: var(--alt-interactive-accent);
|
||||
box-shadow: inset 0px 0px 2px 1px var(--alt-interactive-accent);
|
||||
transition: border-color 0.5s ease-out;
|
||||
}
|
||||
|
||||
#input-display-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<script lang="ts">
|
||||
export let width: string = "18px";
|
||||
export let height: string = "18px";
|
||||
export let editModeActive: boolean = false;
|
||||
export let alternateBackground: boolean = false;
|
||||
export let spinnerElement: HTMLDivElement | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="circle-border"
|
||||
class:edit-mode={editModeActive}
|
||||
style="width: {width}; height: {height};"
|
||||
bind:this={spinnerElement}
|
||||
>
|
||||
|
|
@ -42,10 +40,6 @@
|
|||
animation: spin 0.8s linear 0s infinite;
|
||||
}
|
||||
|
||||
.circle-border.edit-mode {
|
||||
--color: var(--alt-interactive-accent);
|
||||
}
|
||||
|
||||
.circle-core {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="ts">
|
||||
export let editModeActive: boolean = false;
|
||||
export let streamingIndicatorElement: HTMLElement | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<div class="loader" class:edit-mode={editModeActive} bind:this={streamingIndicatorElement}>
|
||||
<div class="loader" bind:this={streamingIndicatorElement}>
|
||||
<div class="circle">
|
||||
<div class="dot"></div>
|
||||
<div class="outline"></div>
|
||||
|
|
@ -33,10 +32,6 @@
|
|||
--animation: 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loader.edit-mode {
|
||||
--color: var(--alt-interactive-accent);
|
||||
}
|
||||
|
||||
.loader .circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import Spinner from "./Spinner.svelte";
|
||||
|
||||
export let thought: string | null = null;
|
||||
export let thoughtIndicatorElement: HTMLElement | undefined = undefined;
|
||||
export let editModeActive : boolean = false;
|
||||
|
||||
$: isVisible = thought !== null && thought.trim().length > 0;
|
||||
|
||||
|
|
@ -13,7 +11,7 @@
|
|||
{#if isVisible}
|
||||
<div class="ai-thought-container" in:fade={{ duration: 200 }} out:fade={{ duration: 200 }} bind:this={thoughtIndicatorElement}>
|
||||
<div class="ai-thought-bubble">
|
||||
<span><Spinner {editModeActive}/> {thought}</span>
|
||||
<span><Spinner/> {thought}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
20
Enums/ChatMode.ts
Normal file
20
Enums/ChatMode.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export enum ChatMode {
|
||||
ReadOnly = 0,
|
||||
Edit = 1,
|
||||
Planning = 2
|
||||
}
|
||||
|
||||
export function chatModeAllowsEdits(mode: ChatMode) {
|
||||
return mode === ChatMode.Edit || mode === ChatMode.Planning;
|
||||
}
|
||||
|
||||
export function iconForChatMode(mode: ChatMode) {
|
||||
switch (mode) {
|
||||
case ChatMode.ReadOnly:
|
||||
return "eye";
|
||||
case ChatMode.Edit:
|
||||
return "pencil";
|
||||
case ChatMode.Planning:
|
||||
return "list-checks";
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,14 @@ export enum Copy {
|
|||
|
||||
SafeContinue= "Continue",
|
||||
|
||||
// Chat Mode Selector
|
||||
ChatModeReadOnlyTitle = "Read-only",
|
||||
ChatModeReadOnlyDesc = "The AI can search and read your vault but cannot make any changes.",
|
||||
ChatModeEditTitle = "Allow Edits",
|
||||
ChatModeEditDesc = "The AI can create, edit, move, and delete files in your vault.",
|
||||
ChatModePlanningTitle = "Planning",
|
||||
ChatModePlanningDesc = "The AI plans its approach before executing tasks in your vault.",
|
||||
|
||||
// Chat Input Placeholders
|
||||
InputPlaceholderQuestion = "Provide an answer...",
|
||||
InputPlaceholderDiff = "Make a suggestion...",
|
||||
|
|
@ -105,8 +113,7 @@ export enum Copy {
|
|||
ButtonSubmitAnswer = "Submit answer",
|
||||
ButtonMakeSuggestion = "Make Suggestion",
|
||||
ButtonSendMessage = "Send Message",
|
||||
ButtonTurnOffAgentMode = "Turn off Agent Mode",
|
||||
ButtonTurnOnAgentMode = "Turn on Agent Mode",
|
||||
ButtonChangeChatMode = "Change the Chat Mode",
|
||||
ButtonTurnOffPlanningMode = "Turn off Planning Mode",
|
||||
ButtonTurnOnPlanningMode = "Turn on Planning Mode",
|
||||
ButtonTurnOffWebSearch = "Turn off Web Search",
|
||||
|
|
|
|||
|
|
@ -14,14 +14,15 @@ import { Role } from "Enums/Role";
|
|||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
import { ChatMode } from "Enums/ChatMode";
|
||||
|
||||
export class MainAgent extends BaseAgent {
|
||||
|
||||
public async runMainAgent(conversation: Conversation, allowDestructiveActions: boolean, planningMode: boolean, callbacks: IChatServiceCallbacks) {
|
||||
await this.setAgentPromptAndTools(planningMode, allowDestructiveActions);
|
||||
this.debugService?.log("MainAgent", `Starting MainAgent (planningMode: ${planningMode}, destructive: ${allowDestructiveActions})`);
|
||||
public async runMainAgent(conversation: Conversation, chatMode: ChatMode, callbacks: IChatServiceCallbacks) {
|
||||
await this.setAgentPromptAndTools(chatMode);
|
||||
this.debugService?.log("MainAgent", `Starting MainAgent (chatMode: ${chatMode})`);
|
||||
|
||||
if (planningMode) {
|
||||
if (chatMode === ChatMode.Planning) {
|
||||
this.debugService?.log("MainAgent", "Planning mode enabled - workflow execution available");
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
|
|
@ -45,7 +46,7 @@ export class MainAgent extends BaseAgent {
|
|||
result.toolCall.toolId
|
||||
));
|
||||
|
||||
await this.setAgentPromptAndTools(planningMode, allowDestructiveActions);
|
||||
await this.setAgentPromptAndTools(chatMode);
|
||||
result = await this.runMainAgentLoop(conversation, callbacks);
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +86,7 @@ export class MainAgent extends BaseAgent {
|
|||
return { planRequest: planRequest, toolCall: planToolCall };
|
||||
}
|
||||
|
||||
private async setAgentPromptAndTools(planningMode: boolean, allowDestructiveActions: boolean): Promise<void> {
|
||||
private async setAgentPromptAndTools(chatMode: ChatMode): Promise<void> {
|
||||
if (!this.ai) { // this shouldn't ever happen
|
||||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
|
|
@ -93,8 +94,8 @@ export class MainAgent extends BaseAgent {
|
|||
this.ai.aiToolUsageMode = AIToolUsageMode.Auto;
|
||||
this.ai.systemPrompt = await this.aiPrompt.systemInstruction();
|
||||
this.ai.userInstruction = await this.aiPrompt.userInstruction();
|
||||
this.ai.aiToolDefinitions = AIToolDefinitions.agentDefinitions(allowDestructiveActions, planningMode,
|
||||
this.memoriesEnabled(), this.updateMemoriesEnabled(), this.webViewerAccessEnabled());
|
||||
this.ai.aiToolDefinitions = AIToolDefinitions.agentDefinitions(
|
||||
chatMode, this.memoriesEnabled(), this.updateMemoriesEnabled(), this.webViewerAccessEnabled());
|
||||
}
|
||||
|
||||
protected override setDebugColor(): void {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Reference } from "Conversations/Reference";
|
|||
import type { WorkSpaceService } from "./WorkSpaceService";
|
||||
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
import type { MainAgent } from "./AIServices/MainAgent";
|
||||
import type { ChatMode } from "Enums/ChatMode";
|
||||
|
||||
export interface IChatServiceCallbacks {
|
||||
onSubmit: () => void;
|
||||
|
|
@ -59,7 +60,7 @@ export class ChatService {
|
|||
|
||||
public onNameChanged: ((name: string) => void) | undefined = undefined;
|
||||
|
||||
public async submit(conversation: Conversation, allowDestructiveActions: boolean, planningMode: boolean, userRequest: string, formattedRequest: string, attachments: Attachment[], callbacks: IChatServiceCallbacks) {
|
||||
public async submit(conversation: Conversation, chatMode: ChatMode, userRequest: string, formattedRequest: string, attachments: Attachment[], callbacks: IChatServiceCallbacks) {
|
||||
if (!await this.semaphore.wait()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -108,7 +109,7 @@ export class ChatService {
|
|||
callbacks.onSubmit();
|
||||
callbacks.onStreamingUpdate(null);
|
||||
|
||||
await this.mainAgent.runMainAgent(conversation, allowDestructiveActions, planningMode, callbacks);
|
||||
await this.mainAgent.runMainAgent(conversation, chatMode, callbacks);
|
||||
|
||||
if (namingPromise) {
|
||||
const timeout = new Promise<void>(resolve => setTimeout(resolve, 5000));
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
}
|
||||
|
||||
.ai-exclusions-input {
|
||||
width: 350px;
|
||||
width: max(350px, 100%);
|
||||
height: 110px;
|
||||
font: var(--font-monospace);
|
||||
font-size: 0.9em;
|
||||
|
|
@ -202,16 +202,6 @@ body {
|
|||
--checkbox-border: var(--background-modifier-border);
|
||||
--checkbox-checked-bg: var(--interactive-accent);
|
||||
--footnote-border: var(--background-modifier-border);
|
||||
--alt-interactive-accent: color-mix(
|
||||
in hsl longer hue,
|
||||
var(--interactive-accent) 50%,
|
||||
hsl(130, 100%, 50%) 50%
|
||||
);
|
||||
--alt-interactive-accent-hover: color-mix(
|
||||
in hsl longer hue,
|
||||
var(--interactive-accent-hover) 50%,
|
||||
hsl(130 100% 50%) 50%
|
||||
);
|
||||
--alt-background-primary: color-mix(
|
||||
in hsl shorter hue,
|
||||
var(--background-primary) 75%,
|
||||
|
|
|
|||
Loading…
Reference in a new issue