mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add user instruction selector with dropdown UI
- Add dropdown interface for selecting custom user instructions - Include "No custom instructions" option to clear selection - Highlight currently active instruction with accent border - Auto-save selected instruction to plugin settings - Close dropdown on click outside or focus loss - Add keyboard navigation with arrow keys and enter - Scroll selected instruction into view automatically - Reset search state when chat input loses focus
This commit is contained in:
parent
c0178dc364
commit
e7516e796e
3 changed files with 71 additions and 19 deletions
|
|
@ -251,6 +251,10 @@
|
|||
searchStateStore.resetSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusOut() {
|
||||
searchStateStore.resetSearch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="input-container" class:edit-mode={editModeActive}>
|
||||
|
|
@ -282,6 +286,7 @@
|
|||
on:drop={handleDrop}
|
||||
on:click={handleCursorPositionChange}
|
||||
on:keyup={handleCursorPositionChange}
|
||||
on:focusout={handleFocusOut}
|
||||
data-placeholder="Type a message..."
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
|
|
|
|||
|
|
@ -10,44 +10,68 @@
|
|||
|
||||
export let userInstructionActive: boolean;
|
||||
|
||||
const plugin: AIAgentPlugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
|
||||
const fileSystemService: FileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
||||
|
||||
let height = 0;
|
||||
|
||||
let emptyResultsContentDiv: HTMLDivElement;
|
||||
let resultsContentDiv: HTMLDivElement;
|
||||
|
||||
let instructionsContentDiv: HTMLDivElement;
|
||||
let emptyInstructionsContentDiv: HTMLDivElement;
|
||||
let instructionsElements: (HTMLDivElement | null)[] = [];
|
||||
let resultsContainer: HTMLDivElement;
|
||||
|
||||
let userInstructions: string[] = [];
|
||||
let selectedInstruction: number = 0;
|
||||
|
||||
$: userInstructions, updateHeight();
|
||||
|
||||
const plugin: AIAgentPlugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
|
||||
const fileSystemService: FileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
||||
$: if (selectedInstruction !== undefined && instructionsElements.length > 0) {
|
||||
scrollSelectedIntoView();
|
||||
}
|
||||
|
||||
$: if (userInstructionActive) {
|
||||
selectedInstruction = 0;
|
||||
loadUserInstructions();
|
||||
setTimeout(() => {
|
||||
if (resultsContainer && userInstructionActive) {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
}
|
||||
}, 10);
|
||||
} else {
|
||||
userInstructions = [];
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
}
|
||||
|
||||
function updateHeight() {
|
||||
tick().then(() => {
|
||||
if (resultsContentDiv) {
|
||||
height = resultsContentDiv.scrollHeight;
|
||||
if (instructionsContentDiv) {
|
||||
height = instructionsContentDiv.scrollHeight;
|
||||
}
|
||||
else if (emptyResultsContentDiv) {
|
||||
height = emptyResultsContentDiv.scrollHeight;
|
||||
else if (emptyInstructionsContentDiv) {
|
||||
height = emptyInstructionsContentDiv.scrollHeight;
|
||||
} else {
|
||||
height = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$: if (userInstructionActive) {
|
||||
loadUserInstructions();
|
||||
} else {
|
||||
userInstructions = [];
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (resultsContainer && !resultsContainer.contains(event.target as Node)) {
|
||||
userInstructionActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserInstructions() {
|
||||
const files = await fileSystemService.listFilesInDirectory(Path.UserInstructions, true, true);
|
||||
userInstructions = files.map(file => file.path).filter(path => path != Path.ExampleUserInstructions);
|
||||
|
||||
if (userInstructions.length > 0) {
|
||||
userInstructions = [Copy.NoUserInstruction, ...userInstructions];
|
||||
}
|
||||
|
||||
tick().then(() => {
|
||||
if (resultsContentDiv) {
|
||||
resultsContentDiv.focus();
|
||||
if (instructionsContentDiv) {
|
||||
instructionsContentDiv.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -55,6 +79,7 @@
|
|||
function handleInstructionSelect() {
|
||||
if (selectedInstruction < userInstructions.length) {
|
||||
plugin.settings.userInstruction = userInstructions[selectedInstruction];
|
||||
plugin.saveSettings();
|
||||
}
|
||||
userInstructionActive = false;
|
||||
}
|
||||
|
|
@ -84,12 +109,24 @@
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollSelectedIntoView() {
|
||||
tick().then(() => {
|
||||
if (selectedInstruction < instructionsElements.length) {
|
||||
instructionsElements[selectedInstruction]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="user-instruction-results" style:height="{height}px">
|
||||
<div id="user-instruction-results" style:height="{height}px" bind:this={resultsContainer}>
|
||||
{#if userInstructionActive}
|
||||
{#if userInstructions.length === 0}
|
||||
<div bind:this={emptyResultsContentDiv}>
|
||||
<div bind:this={emptyInstructionsContentDiv}>
|
||||
<div id="user-instruction-empty" class="user-instruction-container">
|
||||
<div class="user-instruction-title">
|
||||
<span>
|
||||
|
|
@ -104,9 +141,10 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if userInstructions.length > 0}
|
||||
{@const currentInstruction = plugin.settings.userInstruction}
|
||||
<div
|
||||
id="user-instruction-results-inner-container"
|
||||
bind:this={resultsContentDiv}
|
||||
bind:this={instructionsContentDiv}
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
on:keydown={handleKeydown}>
|
||||
|
|
@ -115,12 +153,16 @@
|
|||
role="option"
|
||||
tabindex="-1"
|
||||
aria-selected={selectedInstruction === index}
|
||||
class:current-instruction={currentInstruction === userInstruction}
|
||||
style:background-color={selectedInstruction === index ? "var(--interactive-accent)" : "transparent"}
|
||||
bind:this={instructionsElements[index]}
|
||||
on:mouseenter={() => selectedInstruction = index}
|
||||
on:click={handleInstructionSelect}
|
||||
on:keydown={() => {}}>
|
||||
<div class="user-instruction-title">{basename(userInstruction)}</div>
|
||||
<div class="user-instruction-subtitle">{userInstruction}</div>
|
||||
{#if userInstruction !== basename(userInstruction)}
|
||||
<div class="user-instruction-subtitle">{userInstruction}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -165,6 +207,10 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-instruction-container.current-instruction {
|
||||
box-shadow: inset 0px 0px 4px 1px var(--color-accent);
|
||||
}
|
||||
|
||||
.user-instruction-title {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export enum Copy {
|
|||
UserInstructions1 = "You can create custom ",
|
||||
UserInstructions2 = "instructions",
|
||||
UserInstructions3 = " that the AI will follow.",
|
||||
NoUserInstruction = "No custom instructions",
|
||||
|
||||
// Model display names
|
||||
ClaudeSonnet_4_5 = "Claude Sonnet 4.5",
|
||||
|
|
|
|||
Loading…
Reference in a new issue