2025-11-02 20:16:06 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { Copy } from "Enums/Copy";
|
|
|
|
|
import { Path } from "Enums/Path";
|
2025-11-09 19:59:41 +00:00
|
|
|
import { HelpModal } from "Modals/HelpModal";
|
2025-11-04 19:12:30 +00:00
|
|
|
import { basename } from "path-browserify";
|
2025-11-02 20:16:06 +00:00
|
|
|
import { Resolve } from "Services/DependencyService";
|
|
|
|
|
import type { FileSystemService } from "Services/FileSystemService";
|
2025-11-03 17:14:32 +00:00
|
|
|
import type { SettingsService } from "Services/SettingsService";
|
2025-11-02 20:16:06 +00:00
|
|
|
import { Services } from "Services/Services";
|
|
|
|
|
import { tick } from "svelte";
|
|
|
|
|
|
2025-11-13 21:15:38 +00:00
|
|
|
export let focusInput: () => void;
|
2025-11-02 20:16:06 +00:00
|
|
|
export let userInstructionActive: boolean;
|
|
|
|
|
|
2025-11-03 17:14:32 +00:00
|
|
|
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
|
2025-11-02 23:19:11 +00:00
|
|
|
const fileSystemService: FileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
|
|
|
|
|
2025-11-02 20:16:06 +00:00
|
|
|
let height = 0;
|
2025-11-02 23:19:11 +00:00
|
|
|
|
|
|
|
|
let instructionsContentDiv: HTMLDivElement;
|
|
|
|
|
let emptyInstructionsContentDiv: HTMLDivElement;
|
|
|
|
|
let instructionsElements: (HTMLDivElement | null)[] = [];
|
|
|
|
|
let resultsContainer: HTMLDivElement;
|
2025-11-02 20:16:06 +00:00
|
|
|
|
|
|
|
|
let userInstructions: string[] = [];
|
|
|
|
|
let selectedInstruction: number = 0;
|
|
|
|
|
|
|
|
|
|
$: userInstructions, updateHeight();
|
|
|
|
|
|
2025-11-02 23:19:11 +00:00
|
|
|
$: 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);
|
|
|
|
|
}
|
2025-11-02 20:16:06 +00:00
|
|
|
|
|
|
|
|
function updateHeight() {
|
|
|
|
|
tick().then(() => {
|
2025-11-02 23:19:11 +00:00
|
|
|
if (instructionsContentDiv) {
|
|
|
|
|
height = instructionsContentDiv.scrollHeight;
|
2025-11-02 20:16:06 +00:00
|
|
|
}
|
2025-11-02 23:19:11 +00:00
|
|
|
else if (emptyInstructionsContentDiv) {
|
|
|
|
|
height = emptyInstructionsContentDiv.scrollHeight;
|
2025-11-02 20:16:06 +00:00
|
|
|
} else {
|
|
|
|
|
height = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 23:19:11 +00:00
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
|
|
|
if (resultsContainer && !resultsContainer.contains(event.target as Node)) {
|
|
|
|
|
userInstructionActive = false;
|
|
|
|
|
}
|
2025-11-02 20:16:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadUserInstructions() {
|
|
|
|
|
const files = await fileSystemService.listFilesInDirectory(Path.UserInstructions, true, true);
|
|
|
|
|
userInstructions = files.map(file => file.path).filter(path => path != Path.ExampleUserInstructions);
|
2025-11-02 23:19:11 +00:00
|
|
|
|
|
|
|
|
if (userInstructions.length > 0) {
|
|
|
|
|
userInstructions = [Copy.NoUserInstruction, ...userInstructions];
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 20:16:06 +00:00
|
|
|
tick().then(() => {
|
2025-11-02 23:19:11 +00:00
|
|
|
if (instructionsContentDiv) {
|
|
|
|
|
instructionsContentDiv.focus();
|
2025-11-02 20:16:06 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 21:15:38 +00:00
|
|
|
function handleInstructionSelect(e?: MouseEvent) {
|
2025-11-02 20:16:06 +00:00
|
|
|
if (selectedInstruction < userInstructions.length) {
|
2025-11-03 17:14:32 +00:00
|
|
|
settingsService.settings.userInstruction = userInstructions[selectedInstruction];
|
|
|
|
|
settingsService.saveSettings();
|
2025-11-02 20:16:06 +00:00
|
|
|
}
|
|
|
|
|
userInstructionActive = false;
|
2025-11-13 21:15:38 +00:00
|
|
|
|
|
|
|
|
e?.preventDefault();
|
|
|
|
|
focusInput();
|
2025-11-02 20:16:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleKeydown(e: KeyboardEvent) {
|
|
|
|
|
if (!userInstructionActive) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
if (e.key.startsWith("Arrow")) {
|
|
|
|
|
if (e.key === "ArrowUp") {
|
|
|
|
|
selectedInstruction = selectedInstruction <= 0 ? userInstructions.length - 1 : selectedInstruction - 1;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
selectedInstruction = selectedInstruction >= userInstructions.length - 1 ? 0 : selectedInstruction + 1;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
handleInstructionSelect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
userInstructionActive = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-02 23:19:11 +00:00
|
|
|
|
|
|
|
|
function scrollSelectedIntoView() {
|
|
|
|
|
tick().then(() => {
|
|
|
|
|
if (selectedInstruction < instructionsElements.length) {
|
|
|
|
|
instructionsElements[selectedInstruction]?.scrollIntoView({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
block: 'nearest',
|
|
|
|
|
inline: 'nearest'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-09 19:59:41 +00:00
|
|
|
|
|
|
|
|
function openHelpModal() {
|
|
|
|
|
const modal = Resolve<HelpModal>(Services.HelpModal);
|
|
|
|
|
modal.open(2);
|
|
|
|
|
}
|
2025-11-02 20:16:06 +00:00
|
|
|
</script>
|
|
|
|
|
|
2025-11-02 23:19:11 +00:00
|
|
|
<div id="user-instruction-results" style:height="{height}px" bind:this={resultsContainer}>
|
2025-11-02 20:16:06 +00:00
|
|
|
{#if userInstructionActive}
|
|
|
|
|
{#if userInstructions.length === 0}
|
2025-11-02 23:19:11 +00:00
|
|
|
<div bind:this={emptyInstructionsContentDiv}>
|
2025-11-02 20:16:06 +00:00
|
|
|
<div id="user-instruction-empty" class="user-instruction-container">
|
|
|
|
|
<div class="user-instruction-title">
|
|
|
|
|
<span>
|
|
|
|
|
{Copy.UserInstructions1}
|
2025-11-09 19:59:41 +00:00
|
|
|
<span id="user-instruction-link"
|
|
|
|
|
role="link"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
on:click={openHelpModal}
|
|
|
|
|
on:keydown={openHelpModal}>
|
2025-11-02 20:16:06 +00:00
|
|
|
{Copy.UserInstructions2}
|
|
|
|
|
</span>
|
|
|
|
|
{Copy.UserInstructions3}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if userInstructions.length > 0}
|
2025-11-03 17:14:32 +00:00
|
|
|
{@const currentInstruction = settingsService.settings.userInstruction}
|
2025-11-02 20:16:06 +00:00
|
|
|
<div
|
|
|
|
|
id="user-instruction-results-inner-container"
|
2025-11-02 23:19:11 +00:00
|
|
|
bind:this={instructionsContentDiv}
|
2025-11-02 20:16:06 +00:00
|
|
|
role="listbox"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
on:keydown={handleKeydown}>
|
|
|
|
|
{#each userInstructions as userInstruction, index}
|
|
|
|
|
<div class="user-instruction-container"
|
|
|
|
|
role="option"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
aria-selected={selectedInstruction === index}
|
2025-11-02 23:19:11 +00:00
|
|
|
class:current-instruction={currentInstruction === userInstruction}
|
2025-11-02 20:16:06 +00:00
|
|
|
style:background-color={selectedInstruction === index ? "var(--interactive-accent)" : "transparent"}
|
2025-11-02 23:19:11 +00:00
|
|
|
bind:this={instructionsElements[index]}
|
2025-11-02 20:16:06 +00:00
|
|
|
on:mouseenter={() => selectedInstruction = index}
|
2025-11-04 22:51:22 +00:00
|
|
|
on:mousedown={handleInstructionSelect}
|
2025-11-02 20:16:06 +00:00
|
|
|
on:keydown={() => {}}>
|
|
|
|
|
<div class="user-instruction-title">{basename(userInstruction)}</div>
|
2025-11-02 23:19:11 +00:00
|
|
|
{#if userInstruction !== basename(userInstruction)}
|
|
|
|
|
<div class="user-instruction-subtitle">{userInstruction}</div>
|
|
|
|
|
{/if}
|
2025-11-02 20:16:06 +00:00
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
#user-instruction-results {
|
|
|
|
|
max-height: 15em;
|
|
|
|
|
transition: height 0.2s ease-out;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#user-instruction-results::-webkit-scrollbar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#user-instruction-empty {
|
|
|
|
|
padding-top: var(--size-4-2);
|
|
|
|
|
padding-bottom: var(--size-4-2);
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#user-instruction-results-inner-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--size-2-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user-instruction-container {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-rows: auto auto;
|
|
|
|
|
grid-template-columns: 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 23:19:11 +00:00
|
|
|
.user-instruction-container.current-instruction {
|
|
|
|
|
box-shadow: inset 0px 0px 4px 1px var(--color-accent);
|
2025-11-03 17:14:32 +00:00
|
|
|
border-color: var(--color-accent);
|
2025-11-02 23:19:11 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-02 20:16:06 +00:00
|
|
|
.user-instruction-title {
|
|
|
|
|
grid-row: 1;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
font-family: var(--font-interface-theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user-instruction-subtitle {
|
|
|
|
|
grid-row: 2;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
font-family: var(--font-interface-theme);
|
|
|
|
|
font-size: var(--font-smallest);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#user-instruction-link {
|
|
|
|
|
color: var(--interactive-accent);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#user-instruction-link:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
2025-11-04 22:51:22 +00:00
|
|
|
|
|
|
|
|
:global(.is-mobile) .user-instruction-container[aria-selected="true"] {
|
|
|
|
|
background-color: transparent !important;
|
|
|
|
|
}
|
2025-11-02 20:16:06 +00:00
|
|
|
</style>
|