diff --git a/src/components/ChatPanel.ts b/src/components/ChatPanel.ts index 9816115..77d2aee 100644 --- a/src/components/ChatPanel.ts +++ b/src/components/ChatPanel.ts @@ -209,12 +209,12 @@ export class ChatPanel { ) .join(''); - // Прокручиваем к последнему сообщению + // Прокручиваем к самому низу родительского контейнера (view-content) if (this.chatHistory.length > 0) { setTimeout(() => { - const lastMessage = historyContainer.lastElementChild as HTMLElement; - if (lastMessage) { - lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' }); + const scrollContainer = this.container.closest('.view-content') as HTMLElement; + if (scrollContainer) { + scrollContainer.scrollTop = scrollContainer.scrollHeight; } }, 0); } diff --git a/src/css/styles.css b/src/css/styles.css index b56efae..bd87b98 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -297,11 +297,11 @@ }*/ .message-input-editor { - min-height: 20px; - max-height: 200px; - overflow-y: auto; - word-wrap: break-word; - white-space: pre-wrap; + min-height: 20px; + max-height: 200px; + overflow-y: auto; + word-wrap: break-word; + white-space: pre-wrap; } .message-input-editor.is-empty::before { @@ -312,31 +312,31 @@ } .message-input-editor[data-placeholder]::before { - content: attr(data-placeholder); - color: var(--text-muted); - position: absolute; - pointer-events: none; - opacity: 0.6; + content: attr(data-placeholder); + color: var(--text-muted); + position: absolute; + pointer-events: none; + opacity: 0.6; } .message-input-editor.is-empty[data-placeholder]::before { - display: block; + display: block; } .message-input-editor:not(.is-empty)[data-placeholder]::before, .message-input-editor:focus[data-placeholder]::before { - display: none; + display: none; } .message-input-editor .reference-box { - display: inline-flex !important; - vertical-align: middle; - margin: 0 1px; + display: inline-flex !important; + vertical-align: middle; + margin: 0 1px; } .message-input-editor .reference-box:focus { - outline: 2px solid var(--interactive-accent); - outline-offset: 1px; + outline: 2px solid var(--interactive-accent); + outline-offset: 1px; } .chat-controls { @@ -433,8 +433,10 @@ } .model-option.selected { - background-color: var(--background-modifier-selected); /* Или другой подходящий цвет */ - color: var(--text-highlighted); /* Или другой подходящий цвет */ + background-color: var( + --background-modifier-selected + ); /* Или другой подходящий цвет */ + color: var(--text-highlighted); /* Или другой подходящий цвет */ } .model-option:first-child { @@ -497,8 +499,8 @@ } .suggestion-item.selected { - background-color: var(--background-modifier-selected); - color: var(--text-highlighted); + background-color: var(--background-modifier-selected); + color: var(--text-highlighted); } /* Удаляем старые стили для chat-input-form и message-input */ @@ -534,11 +536,11 @@ /* Стили для заголовков графов в панели истории */ .graph-title-generated { - color: var(--text-normal); + color: var(--text-normal); } .graph-title-pending { - color: var(--text-muted); + color: var(--text-muted); } /* ----------------------------------------------- Toggle Button Styles --------------------------------------------------- */ @@ -581,212 +583,233 @@ /* Reference Boxes */ .reference-box { - display: inline-flex; - align-items: center; - padding: 2px 6px; - border-radius: 4px; - font-size: 11px; - margin: 0 2px; - cursor: pointer; - user-select: none; - vertical-align: middle; - transition: all 0.2s ease; + display: inline-flex; + align-items: center; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + margin: 0 2px; + cursor: pointer; + user-select: none; + vertical-align: middle; + transition: all 0.2s ease; } .reference-box:hover { - opacity: 0.8; - transform: translateY(-1px); + opacity: 0.8; + transform: translateY(-1px); } .reference-box-icon { - width: 12px; - height: 12px; - margin-right: 4px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; + width: 12px; + height: 12px; + margin-right: 4px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; } .reference-box-name { - font-weight: 500; - white-space: nowrap; + font-weight: 500; + white-space: nowrap; } /* Файлы - одинарная ссылка @ */ .reference-box.file.single { - background: rgba(100, 150, 255, 0.1); - border: 1px solid rgba(100, 150, 255, 0.3); - color: var(--text-normal); + background: rgba(100, 150, 255, 0.1); + border: 1px solid rgba(100, 150, 255, 0.3); + color: var(--text-normal); } /* Файлы - постоянная ссылка @@ */ .reference-box.file.permanent { - background: rgba(100, 150, 255, 0.2); - border: 1px solid rgba(100, 150, 255, 0.6); - color: var(--text-normal); - font-weight: 600; + background: rgba(100, 150, 255, 0.2); + border: 1px solid rgba(100, 150, 255, 0.6); + color: var(--text-normal); + font-weight: 600; } .reference-box.file.permanent .reference-box-name { - font-weight: 600; + font-weight: 600; } /* Перебиваем стиль Obsidian для prompt*/ .reference-box.prompt { - display: inline-flex; - flex-direction: row; + display: inline-flex; + flex-direction: row; - box-shadow: var(--shadow-l); + box-shadow: var(--shadow-l); - z-index: auto; - position: static; - top: auto; - width: auto; - max-width: none; - max-height: none; - overflow: hidden; + z-index: auto; + position: static; + top: auto; + width: auto; + max-width: none; + max-height: none; + overflow: hidden; } /* Промпты - одинарная ссылка # */ .reference-box.prompt.single { - background: rgba(255, 150, 100, 0.1); - border: 1px solid rgba(255, 150, 100, 0.3); - color: var(--text-normal); + background: rgba(255, 150, 100, 0.1); + border: 1px solid rgba(255, 150, 100, 0.3); + color: var(--text-normal); } /* Промпты - постоянная ссылка ## */ .reference-box.prompt.permanent { - background: rgba(238, 122, 69, 0.2); - border: 1px solid rgba(255, 150, 100, 0.6); - color: var(--text-normal); - font-weight: 600; + background: rgba(238, 122, 69, 0.2); + border: 1px solid rgba(255, 150, 100, 0.6); + color: var(--text-normal); + font-weight: 600; } .reference-box.prompt.permanent .reference-box-name { - font-weight: 600; + font-weight: 600; } /* Reference Autocomplete */ .reference-autocomplete { - position: absolute; - background: var(--background-primary); - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - max-height: 200px; - overflow-y: auto; - min-width: 250px; - max-width: 400px; + position: absolute; + background: var(--background-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + max-height: 200px; + overflow-y: auto; + min-width: 250px; + max-width: 400px; } .reference-option { - display: flex; - align-items: center; - padding: 8px 12px; - cursor: pointer; - font-size: 12px; - color: var(--text-normal); - transition: background-color 0.2s; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + padding: 8px 12px; + cursor: pointer; + font-size: 12px; + color: var(--text-normal); + transition: background-color 0.2s; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); } .reference-option:hover, .reference-option.selected { - background-color: var(--background-modifier-hover); + background-color: var(--background-modifier-hover); } .reference-option:last-child { - border-bottom: none; + border-bottom: none; } .reference-option-icon { - width: 16px; - height: 16px; - margin-right: 8px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); + width: 16px; + height: 16px; + margin-right: 8px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); } .reference-option-name { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 500; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; } .reference-option-path { - font-size: 10px; - color: var(--text-muted); - margin-left: 8px; - max-width: 150px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + font-size: 10px; + color: var(--text-muted); + margin-left: 8px; + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* Темная тема */ .theme-dark .reference-autocomplete { - background: var(--background-primary); - border-color: var(--border-color); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + background: var(--background-primary); + border-color: var(--border-color); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .theme-dark .reference-option:hover, .theme-dark .reference-option.selected { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.1); } /* Светлая тема */ .theme-light .reference-autocomplete { - background: #ffffff; - border-color: #e0e0e0; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + background: #ffffff; + border-color: #e0e0e0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .theme-light .reference-option:hover, .theme-light .reference-option.selected { - background-color: rgba(0, 0, 0, 0.05); + background-color: rgba(0, 0, 0, 0.05); } /* Скроллбар для автокомплита */ .reference-autocomplete::-webkit-scrollbar { - width: 6px; + width: 6px; } .reference-autocomplete::-webkit-scrollbar-track { - background: transparent; + background: transparent; } .reference-autocomplete::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb-bg); - border-radius: 3px; + background: var(--scrollbar-thumb-bg); + border-radius: 3px; } .reference-autocomplete::-webkit-scrollbar-thumb:hover { - background: var(--scrollbar-thumb-bg-hover); + background: var(--scrollbar-thumb-bg-hover); } /* ----------------------------------------------- Settings Views ------------------------------------------------------------ */ .setting-item.llm-agent-excluded-folder-item { - padding-left: 30px; /* Отступ для элементов списка */ - border-left: 2px solid var(--background-modifier-border); /* Разделитель */ - margin-bottom: 5px; - margin-top: 5px; + padding-left: 30px; /* Отступ для элементов списка */ + border-left: 2px solid var(--background-modifier-border); /* Разделитель */ + margin-bottom: 5px; + margin-top: 5px; } .setting-item.llm-agent-excluded-folder-item .setting-item-control { - display: flex; - align-items: center; - gap: 5px; + display: flex; + align-items: center; + gap: 5px; } .setting-item.llm-agent-excluded-folder-item .setting-item-control input { - flex-grow: 1; /* Поле ввода занимает все доступное пространство */ -} \ No newline at end of file + flex-grow: 1; /* Поле ввода занимает все доступное пространство */ +} + +/* ----------------------------------------------- Obsidian core override ------------------------------------------------------------ */ + +.status-bar-item { + display: none; + padding: 0; + + &.plugin-editor-width-slider { + display: inline-flex; + + & .editor-width-slider { + width: 80px !important; + } + + & .editor-width-slider-value { + padding: 4px 3px !important; + font-size: 10px !important; + margin: 0 !important; + } + } +} diff --git a/src/references/ReferenceAutocomplete.ts b/src/references/ReferenceAutocomplete.ts index e2c3325..3ae7be9 100644 --- a/src/references/ReferenceAutocomplete.ts +++ b/src/references/ReferenceAutocomplete.ts @@ -31,33 +31,34 @@ export class ReferenceAutocomplete { // ----------------------------------------------- Public Methods --------------------------------------------------------------- - /** - * Показывает автокомплит с опциями. - * @param reference - объект ReferenceMatch, определяющий текущий вводимый референс. - * @param cursorRect - DOMRect объекта курсора, для определения позиции. - */ - async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise { - this.currentReference = reference; - - // Получаем опции в зависимости от типа ссылки - const options = reference.type === 'file' - ? await this.autocompleteManager.getFileOptions(reference.name) - : await this.autocompleteManager.getPromptOptions(reference.name); - - if (options.length === 0) { - this.hideAutocomplete(); - return; - } - - this.currentOptions = options; - // Сбрасываем selection, если новые опции не содержат предыдущий выбранный элемент - if (this.selectedIndex >= this.currentOptions.length) { - this.selectedIndex = 0; - } - this.renderOptions(); - this.positionDropdown(cursorRect); - this.dropdown.style.display = 'block'; - } + /** + * Показывает автокомплит с опциями. + * @param reference - объект ReferenceMatch, определяющий текущий вводимый референс. + * @param cursorRect - DOMRect объекта курсора, для определения позиции. + */ + async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise { + this.currentReference = reference; + + // Получаем опции в зависимости от типа ссылки + const options = + reference.type === 'file' + ? await this.autocompleteManager.getFileOptions(reference.name) + : await this.autocompleteManager.getPromptOptions(reference.name); + + if (options.length === 0) { + this.hideAutocomplete(); + return; + } + + this.currentOptions = options; + // Сбрасываем selection, если новые опции не содержат предыдущий выбранный элемент + if (this.selectedIndex >= this.currentOptions.length) { + this.selectedIndex = 0; + } + this.renderOptions(); + this.positionDropdown(cursorRect); + this.dropdown.style.display = 'block'; + } /** * Скрывает автокомплит @@ -74,39 +75,39 @@ export class ReferenceAutocomplete { * @param event - событие клавиатуры * @returns true если событие было обработано */ - handleKeydown(event: KeyboardEvent): boolean { - // Мы хотим, чтобы стрелки и Enter обрабатывались только если автокомплит видим - if (!this.isVisible()) { - return false; - } + handleKeydown(event: KeyboardEvent): boolean { + // Мы хотим, чтобы стрелки и Enter обрабатывались только если автокомплит видим + if (!this.isVisible()) { + return false; + } - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); // Предотвращаем прокрутку страницы - this.moveSelection(1); - return true; - - case 'ArrowUp': - event.preventDefault(); // Предотвращаем прокрутку страницы - this.moveSelection(-1); - return true; - - case 'Enter': - case 'Tab': - // Разрешаем Tab завершать автокомплит - event.preventDefault(); - this.selectCurrentItem(); - return true; - - case 'Escape': - event.preventDefault(); - this.hideAutocomplete(); - return true; - - default: - return false; - } - } + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); // Предотвращаем прокрутку страницы + this.moveSelection(1); + return true; + + case 'ArrowUp': + event.preventDefault(); // Предотвращаем прокрутку страницы + this.moveSelection(-1); + return true; + + case 'Enter': + case 'Tab': + // Разрешаем Tab завершать автокомплит + event.preventDefault(); + this.selectCurrentItem(); + return true; + + case 'Escape': + event.preventDefault(); + this.hideAutocomplete(); + return true; + + default: + return false; + } + } /** * Устанавливает коллбек для выбора элемента @@ -141,43 +142,43 @@ export class ReferenceAutocomplete { /** * Настраивает обработчики событий */ - private setupEventListeners(): void { - // Закрытие при клике вне dropdown - document.addEventListener('click', (e) => { - if (!this.dropdown.contains(e.target as Node) && !this.inputElement.contains(e.target as Node)) { - this.hideAutocomplete(); - } - }); + private setupEventListeners(): void { + // Закрытие при клике вне dropdown + document.addEventListener('click', (e) => { + if (!this.dropdown.contains(e.target as Node) && !this.inputElement.contains(e.target as Node)) { + this.hideAutocomplete(); + } + }); - // Отслеживание наведения мыши для предотвращения скрытия по blur - this.dropdown.addEventListener('mouseenter', () => { - this.mouseIsOverDropdown = true; - }); - this.dropdown.addEventListener('mouseleave', () => { - this.mouseIsOverDropdown = false; - }); + // Отслеживание наведения мыши для предотвращения скрытия по blur + this.dropdown.addEventListener('mouseenter', () => { + this.mouseIsOverDropdown = true; + }); + this.dropdown.addEventListener('mouseleave', () => { + this.mouseIsOverDropdown = false; + }); - // Скрытие при потере фокуса input элементом, если мышь не над дропдауном - this.inputElement.addEventListener('blur', () => { - // Задержка позволяет клику на элемент дропдауна сработать - if (!this.mouseIsOverDropdown) { - this.hideAutocomplete(); - } - }); - } + // Скрытие при потере фокуса input элементом, если мышь не над дропдауном + this.inputElement.addEventListener('blur', () => { + // Задержка позволяет клику на элемент дропдауна сработать + if (!this.mouseIsOverDropdown) { + this.hideAutocomplete(); + } + }); + } /** * Отрисовывает опции в dropdown */ - private renderOptions(): void { - this.dropdown.innerHTML = ''; - - this.currentOptions.forEach((option, index) => { - const optionElement = this.createOptionElement(option, index === this.selectedIndex); - this.dropdown.appendChild(optionElement); - }); - this.scrollToSelected(); // Убеждаемся, что выбранный элемент виден - } + private renderOptions(): void { + this.dropdown.innerHTML = ''; + + this.currentOptions.forEach((option, index) => { + const optionElement = this.createOptionElement(option, index === this.selectedIndex); + this.dropdown.appendChild(optionElement); + }); + this.scrollToSelected(); // Убеждаемся, что выбранный элемент виден + } /** * Создает элемент опции @@ -220,47 +221,51 @@ export class ReferenceAutocomplete { /** * Позиционирует dropdown относительно курсора - * @param position - желаемая позиция + * @param cursorRect - DOMRect объекта курсора, для определения позиции. */ - private positionDropdown(cursorRect: DOMRect): void { - const inputRect = this.inputElement.getBoundingClientRect(); - const containerRect = this.container.getBoundingClientRect(); - const dropdownRect = this.dropdown.getBoundingClientRect(); // Получаем размеры дропдауна - const viewportHeight = window.innerHeight; - - // Позиционируем относительно inputElement, т.к. он уже в потоке документа - // Левый край дропдауна совпадает с левым краем курсора - let left = cursorRect.left - containerRect.left; - - // Дефолтное положение - под курсором - let top = cursorRect.bottom - containerRect.top + 5; // +5px отступ - - // Проверяем, есть ли место снизу - const spaceBelow = viewportHeight - cursorRect.bottom; - const spaceAbove = cursorRect.top; - - if (spaceBelow < dropdownRect.height + 10 && spaceAbove > dropdownRect.height + 10) { - // Если снизу мало места и сверху достаточно, размещаем сверху - top = cursorRect.top - containerRect.top - dropdownRect.height - 5; // -5px отступ - } + private positionDropdown(cursorRect: DOMRect): void { + // Чтобы получить корректные размеры, устанавливаем display: 'block' + this.dropdown.style.display = 'block'; - // Убедимся, что дропдаун не выходит за границы контейнера справа - const maxRight = inputRect.right - containerRect.left; - if (left + dropdownRect.width > maxRight) { - left = maxRight - dropdownRect.width; - } - // Убедимся, что дропдаун не выходит за границы контейнера слева - if (left < 0) { - left = 0; - } + const inputRect = this.inputElement.getBoundingClientRect(); + const containerRect = this.container.getBoundingClientRect(); + const dropdownRect = this.dropdown.getBoundingClientRect(); // Получаем размеры дропдауна + const viewportHeight = window.innerHeight; - this.dropdown.style.top = `${top}px`; - this.dropdown.style.left = `${left}px`; - // Устанавливаем ширину дропдауна равной ширине inputElement, если он больше - // Это предотвратит слишком узкий дропдаун, если имя файла короткое - this.dropdown.style.minWidth = `${inputRect.width / 2}px`; // Минимум половина ширины инпута - this.dropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth - } + // Позиционируем относительно inputElement, т.к. он уже в потоке документа + // Левый край дропдауна совпадает с левым краем курсора + let left = cursorRect.left - containerRect.left; + + // Дефолтное положение - под курсором + let top = cursorRect.bottom - containerRect.top + 5; // +5px отступ + + // Проверяем, есть ли место снизу + const spaceBelow = viewportHeight - cursorRect.bottom; + const spaceAbove = cursorRect.top; + + if (spaceBelow < dropdownRect.height + 10 && spaceAbove > dropdownRect.height + 10) { + // Если снизу мало места и сверху достаточно, размещаем сверху + top = cursorRect.top - containerRect.top - dropdownRect.height - 5; // -5px отступ + } + + // Убедимся, что дропдаун не выходит за границы контейнера справа + const maxRight = inputRect.right - containerRect.left; + if (left + dropdownRect.width > maxRight) { + left = maxRight - dropdownRect.width; + if (left < 0) left = 0; // Если дропдаун шире контейнера, прижимаем к левому краю + } + // Убедимся, что дропдаун не выходит за границы контейнера слева + if (left < 0) { + left = 0; + } + + this.dropdown.style.top = `${top}px`; + this.dropdown.style.left = `${left}px`; + // Устанавливаем ширину дропдауна равной ширине inputElement, если он больше + // Это предотвратит слишком узкий дропдаун, если имя файла короткое + this.dropdown.style.minWidth = `${inputRect.width / 2}px`; // Минимум половина ширины инпута + this.dropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth + } /** * Перемещает выделение в списке