-
`;
- this.messageInput = this.container.querySelector('#message-input') as HTMLInputElement;
+ this.messageInput = this.container.querySelector('.message-input-editor') as HTMLDivElement;
this.sendButton = this.container.querySelector('#send-button') as HTMLButtonElement;
this.suggestionsContainer = this.container.querySelector('#suggestions') as HTMLDivElement;
+ this.modelSelectButton = this.container.querySelector('#model-select-button') as HTMLButtonElement;
+ this.modelDropdown = this.container.querySelector('#model-dropdown') as HTMLDivElement;
this.setupEventListeners();
this.renderHistory();
+ this.updateModelDropdown();
+ this.updatePlaceholder();
}
setupEventListeners(): void {
this.sendButton.addEventListener('click', () => this.handleSend());
this.messageInput.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
+ if (e.key === 'Enter' && !e.shiftKey) {
if (this.suggestionsContainer.style.display === 'block') {
const firstSuggestion = this.suggestionsContainer.querySelector('.suggestion-item');
if (firstSuggestion) {
- this.messageInput.value = firstSuggestion.textContent || '';
+ this.messageInput.textContent = firstSuggestion.textContent || '';
this.hideSuggestions();
e.preventDefault();
return;
@@ -71,12 +106,116 @@ export class ChatPanel {
}
});
- this.messageInput.addEventListener('input', (e) => {
- this.handleInput((e.target as HTMLInputElement).value);
+ this.messageInput.addEventListener('input', () => {
+ this.handleInput();
+ this.updatePlaceholder();
+ });
+
+ this.messageInput.addEventListener('focus', () => {
+ this.updatePlaceholder();
+ });
+
+ this.messageInput.addEventListener('blur', () => {
+ this.updatePlaceholder();
+ });
+
+ // Model selection
+ this.modelSelectButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.toggleModelDropdown();
+ });
+
+ // Close dropdown when clicking outside
+ document.addEventListener('click', (e) => {
+ if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
+ this.modelDropdown.style.display = 'none';
+ }
+ if (!this.suggestionsContainer.contains(e.target as Node)) {
+ this.hideSuggestions();
+ }
});
}
- handleInput(value: string): void {
+ updatePlaceholder(): void {
+ const isEmpty = this.messageInput.textContent?.trim() === '';
+ const isFocused = document.activeElement === this.messageInput;
+
+ if (isEmpty && !isFocused) {
+ this.messageInput.classList.add('is-empty');
+ } else {
+ this.messageInput.classList.remove('is-empty');
+ }
+ }
+
+ toggleModelDropdown(): void {
+ const isVisible = this.modelDropdown.style.display === 'block';
+ if (isVisible) {
+ this.modelDropdown.style.display = 'none';
+ return;
+ }
+
+ // Показываем дропдаун, чтобы получить его размеры и положение
+ this.modelDropdown.style.display = 'block';
+
+ const modelSelectButtonRect = this.modelSelectButton.getBoundingClientRect();
+ const modelDropdownRect = this.modelDropdown.getBoundingClientRect();
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
+
+ // ----------------------------------------------- Секция логики позиционирования дропдауна ----------------------------------------------------
+ // Определяем, достаточно ли места снизу
+ const spaceBelow = viewportHeight - modelSelectButtonRect.bottom;
+ // Определяем, достаточно ли места сверху
+ const spaceAbove = modelSelectButtonRect.top;
+
+ if (spaceBelow >= modelDropdownRect.height || spaceBelow >= spaceAbove) {
+ // Открываем вниз, если есть достаточно места или места снизу больше
+ this.modelDropdown.style.top = `${modelSelectButtonRect.height + 4}px`; // 4px отступ от кнопки
+ this.modelDropdown.style.bottom = 'auto';
+ } else {
+ // Открываем вверх
+ this.modelDropdown.style.bottom = `${modelSelectButtonRect.height + 4}px`; // 4px отступ от кнопки
+ this.modelDropdown.style.top = 'auto';
+ }
+ this.modelDropdown.style.left = '0';
+ this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно
+ // ----------------------------------------------- End of Секция логики позиционирования дропдауна ---------------------------------------------
+ }
+
+ updateModelDropdown(): void {
+ if (!this.props.availableModels) return;
+
+ this.modelDropdown.innerHTML = this.props.availableModels
+ .map(
+ (model) => `
+
+ ${model}
+
+ `
+ )
+ .join('');
+
+ // Add event listeners to model options
+ this.modelDropdown.querySelectorAll('.model-option').forEach((option) => {
+ option.addEventListener('click', () => {
+ const model = (option as HTMLElement).dataset.model;
+ if (model) {
+ this.setSelectedModel(model);
+ }
+ });
+ });
+ }
+
+ setSelectedModel(model: string): void {
+ this.selectedModel = model;
+ const modelNameElement = this.container.querySelector('#selected-model-name');
+ if (modelNameElement) {
+ modelNameElement.textContent = model;
+ }
+ this.modelDropdown.style.display = 'none';
+ }
+
+ handleInput(): void {
+ const value = this.messageInput.textContent || '';
const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat'];
if (value.startsWith('/')) {
@@ -100,7 +239,7 @@ export class ChatPanel {
// Добавляем обработчики кликов на предложения
this.suggestionsContainer.querySelectorAll('.suggestion-item').forEach((item) => {
item.addEventListener('click', () => {
- this.messageInput.value = item.textContent || '';
+ this.messageInput.textContent = item.textContent || '';
this.hideSuggestions();
this.messageInput.focus();
});
@@ -112,10 +251,11 @@ export class ChatPanel {
}
handleSend(): void {
- const message = this.messageInput.value.trim();
+ const message = this.messageInput.textContent?.trim() || '';
if (message && this.props.onSendMessage) {
- this.props.onSendMessage(message);
- this.messageInput.value = '';
+ this.props.onSendMessage(message, this.selectedModel);
+ this.messageInput.textContent = '';
+ this.updatePlaceholder();
this.hideSuggestions();
}
}
@@ -181,6 +321,11 @@ export class ChatPanel {
this.renderHistory();
}
+ updateAvailableModels(models: string[]): void {
+ this.props.availableModels = models;
+ this.updateModelDropdown();
+ }
+
destroy(): void {
this.container.innerHTML = '';
}
diff --git a/src/css/styles.css b/src/css/styles.css
index 18024e0..ae4b24c 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -194,38 +194,210 @@
scroll-margin-bottom: 30px; /* Добавляет отступ в 20px снизу при прокрутке к этому элементу */
}
-#send-button {
- padding: 8px 15px;
- background-color: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
+.submit-button {
+ padding: 4px 8px; /* Восстанавливаем паддинг */
+ border: none; /* Убираем рамку */
+ border-radius: 8px;
cursor: pointer;
+ font-size: 11px;
+ transition: all 0.2s;
+ /* Явно переопределяем стили, которые могли быть затронуты */
+ width: auto; /* Убедимся, что ширина не ограничена */
+ height: auto; /* Убедимся, что высота не ограничена */
+ background-color: #0078d4 !important; /* Или ваш основной цвет кнопки */
+ color: white !important; /* Цвет текста на кнопке */
+ opacity: 1; /* Если .clickable-icon устанавливает opacity */
+ display: inline-flex; /* Или flex, если нужно выравнивание элементов внутри */
+ align-items: center; /* Для вертикального выравнивания контента, если display: flex */
+ justify-content: center; /* Для горизонтального выравнивания контента, если display: flex */
}
#send-button:hover {
- background-color: #0056b3;
+ background-color: #0056b3 !important;
}
+
+/* Добавляем стили для нового интерфейса ввода */
+
+.chat-input-container {
+ border-top: 1px solid var(--background-modifier-border);
+ padding: 8px 10px 4px 10px;
+ position: relative;
+}
+
+.scroll-container {
+ overflow-y: auto;
+ scrollbar-width: thin;
+ max-height: 70vh;
+ margin-bottom: 8px;
+}
+
+.message-input-editor {
+ outline: none;
+ font-size: 14px;
+ line-height: 1.4;
+ min-height: 20px;
+ padding: 8px 0;
+ color: var(--text-normal);
+ position: relative;
+}
+
+.message-input-editor.is-empty::before {
+ content: attr(data-placeholder);
+ color: var(--text-muted);
+ position: absolute;
+ pointer-events: none;
+}
+
+.chat-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.control-buttons-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.control-buttons-right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.model-select-container {
+ position: relative;
+}
+
+.model-select-button {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ border: none;
+ border-radius: 8px;
+ background-color: var(--background-modifier-form-field);
+ color: var(--text-muted);
+ cursor: pointer;
+ font-size: 11px;
+ transition: all 0.2s;
+}
+
+.model-select-button:hover {
+ background-color: var(--background-modifier-form-field-highlighted);
+ filter: brightness(1.1);
+}
+
+.model-name {
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dropdown-icon {
+ width: 12px;
+ height: 12px;
+ flex-shrink: 0;
+}
+
+.model-dropdown {
+ position: absolute;
+ /* Убираем жесткое bottom: 100%; т.к. позиционирование будет задаваться в JS */
+ /* bottom: 100%; */
+ left: 0;
+ /* right: 0; */ /* Это может привести к растягиванию, лучше оставить auto или задать ширину */
+ width: max-content; /* Чтобы дропдаун не растягивался на всю ширину родителя */
+ min-width: 150px; /* Минимальная ширина */
+ background-color: var(--background-primary);
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 1000;
+ /* margin-bottom: 4px; */ /* Это будет задавать JS через top/bottom */
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.model-option {
+ padding: 8px 12px;
+ cursor: pointer;
+ font-size: 11px;
+ color: var(--text-normal);
+ transition: background-color 0.2s;
+}
+
+.model-option:hover {
+
+}
+
+.model-option:first-child {
+ border-radius: 8px 8px 0 0;
+}
+
+.model-option:last-child {
+ border-radius: 0 0 8px 8px;
+}
+
+.submit-button {
+ padding: 4px 8px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 11px;
+ transition: all 0.2s;
+}
+
+.submit-button:hover:enabled {
+ filter: brightness(1.25);
+}
+
+.submit-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.submit-text {
+ font-family: system-ui, -apple-system, sans-serif;
+}
+
+/* Обновляем стили для suggestions */
.suggestions {
position: absolute;
- bottom: 100%; /* Помещаем над полем ввода */
+ bottom: 100%;
left: 0;
right: 0;
- background-color: white;
- border: 1px solid #ccc;
- border-radius: 4px;
+ background-color: var(--background-primary);
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 8px;
max-height: 150px;
overflow-y: auto;
- z-index: 10;
- margin-bottom: 5px; /* Отступ от поля ввода */
+ z-index: 100;
+ margin-bottom: 4px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.suggestion-item {
- padding: 8px 10px;
+ padding: 8px 12px;
cursor: pointer;
+ font-size: 12px;
+ color: var(--text-normal);
+ transition: background-color 0.2s;
}
.suggestion-item:hover {
- background-color: #f0f0f0;
-}
\ No newline at end of file
+}
+
+/* Удаляем старые стили для chat-input-form и message-input */
+.chat-input-form {
+ display: none;
+}
+
+#message-input {
+ display: none;
+}
+
diff --git a/src/views/ChatView.ts b/src/views/ChatView.ts
index 6e94607..8689a1f 100644
--- a/src/views/ChatView.ts
+++ b/src/views/ChatView.ts
@@ -18,6 +18,7 @@ export class ChatView extends ItemView {
currentNode: string | null;
chatContainer: HTMLDivElement;
chatPanel: ChatPanel | null;
+ availableModels: string[];
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf);
@@ -26,6 +27,7 @@ export class ChatView extends ItemView {
this.selectedGraphId = null;
this.currentNode = null;
this.chatPanel = null;
+ this.availableModels = [];
}
getViewType(): string {
@@ -48,10 +50,15 @@ export class ChatView extends ItemView {
this.chatContainer = container.createDiv() as HTMLDivElement;
this.chatContainer.addClass('llm-agent-chat-container');
+ // Загружаем доступные модели
+ await this.fetchAvailableModels();
+
// Инициализируем только чат панель
this.chatPanel = new ChatPanel(this.chatContainer, {
chatHistory: this.chatHistory,
- onSendMessage: this.handleSendMessage.bind(this)
+ onSendMessage: this.handleSendMessage.bind(this),
+ availableModels: this.availableModels,
+ selectedModel: this.plugin.settings.defaultModel || 'gemini-2.5-flash'
});
// Подписываемся на события
@@ -113,9 +120,27 @@ export class ChatView extends ItemView {
}
}
+ /**
+ * Загружает список доступных моделей с бэкенда.
+ * @returns {Promise
}
+ */
+ async fetchAvailableModels(): Promise {
+ try {
+ const response = await fetch(`${this.plugin.settings.apiBaseUrl}/models`);
+ if (!response.ok) {
+ throw new Error(`Ошибка загрузки моделей: ${response.status}`);
+ }
+ this.availableModels = await response.json();
+ } catch (error) {
+ console.error('Ошибка загрузки доступных моделей:', error);
+ // Fallback к базовым моделям
+ this.availableModels = ['gemini-2.5-flash', 'gpt-4o-mini', 'claude-3-5-sonnet-20241022'];
+ }
+ }
+
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
- async handleSendMessage(message: string): Promise {
+ async handleSendMessage(message: string, selectedModel?: string): Promise {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
method: 'POST',
@@ -126,7 +151,8 @@ export class ChatView extends ItemView {
message: message,
graph_id: this.selectedGraphId,
parent_node_id: this.currentNode,
- system_prompt: this.plugin.settings.systemPrompt
+ system_prompt: this.plugin.settings.systemPrompt,
+ model: selectedModel || 'gemini-2.5-flash'
})
});