Add voice dictation to chat panel and global stop generation button.
This commit is contained in:
parent
29a47ed28d
commit
205040a599
31
main.ts
31
main.ts
|
|
@ -46,6 +46,8 @@ interface LLMAgentSettings {
|
||||||
voskModelPath: string;
|
voskModelPath: string;
|
||||||
showVoicePanel: boolean;
|
showVoicePanel: boolean;
|
||||||
|
|
||||||
|
dictationStartAction: 'start' | 'continue';
|
||||||
|
|
||||||
lastActiveGraphId: string | null;
|
lastActiveGraphId: string | null;
|
||||||
lastActiveNodeId: string | null;
|
lastActiveNodeId: string | null;
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +84,8 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||||
voskModelPath: '',
|
voskModelPath: '',
|
||||||
showVoicePanel: false,
|
showVoicePanel: false,
|
||||||
|
|
||||||
|
dictationStartAction: 'start',
|
||||||
|
|
||||||
lastActiveGraphId: null,
|
lastActiveGraphId: null,
|
||||||
lastActiveNodeId: null,
|
lastActiveNodeId: null,
|
||||||
};
|
};
|
||||||
|
|
@ -190,6 +194,15 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
this.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
|
this.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
|
||||||
this.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
|
this.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
|
||||||
|
|
||||||
|
this.eventBus.on('stop-all-streams', () => {
|
||||||
|
this.activeStreamControllers.forEach((controller, nodeId) => {
|
||||||
|
controller.abort();
|
||||||
|
});
|
||||||
|
this.activeStreamControllers.clear();
|
||||||
|
new Notice('Отменены все генерации во всех графах', 2000);
|
||||||
|
this.eventBus.emit('global-stream-state-changed', false);
|
||||||
|
});
|
||||||
|
|
||||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||||
|
|
||||||
|
|
@ -274,10 +287,14 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
|
|
||||||
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
|
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
|
||||||
this.activeStreamControllers.set(data.nodeId, data.controller);
|
this.activeStreamControllers.set(data.nodeId, data.controller);
|
||||||
|
this.eventBus.emit('global-stream-state-changed', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStreamEnded(data: { nodeId: string }): void {
|
handleStreamEnded(data: { nodeId: string }): void {
|
||||||
this.activeStreamControllers.delete(data.nodeId);
|
this.activeStreamControllers.delete(data.nodeId);
|
||||||
|
if (this.activeStreamControllers.size === 0) {
|
||||||
|
this.eventBus.emit('global-stream-state-changed', false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,6 +499,20 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Действие диктовки')
|
||||||
|
.setDesc('При нажатии записи в чате, если голос не запущен, как его стартовать?')
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown.addOption('start', 'Начать заново (Start New)');
|
||||||
|
dropdown.addOption('continue', 'Продолжить (Continue)');
|
||||||
|
dropdown.setValue(this.plugin.settings.dictationStartAction)
|
||||||
|
.onChange(async (value: 'start' | 'continue') => {
|
||||||
|
this.plugin.settings.dictationStartAction = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
await this.plugin.syncSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Папка аудио-логов')
|
.setName('Папка аудио-логов')
|
||||||
.setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.')
|
.setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.')
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,17 @@ export class ChatPanel {
|
||||||
attachedFilesContainer: HTMLDivElement;
|
attachedFilesContainer: HTMLDivElement;
|
||||||
currentAttachments: Attachment[] = [];
|
currentAttachments: Attachment[] = [];
|
||||||
|
|
||||||
|
globalStopBtn: HTMLButtonElement;
|
||||||
|
dictationBtn: HTMLButtonElement;
|
||||||
|
|
||||||
|
// Состояние диктовки
|
||||||
|
isDictating: boolean = false;
|
||||||
|
dictationPollTimer: number | null = null;
|
||||||
|
dictationBuffer: string = "";
|
||||||
|
dictationSessionId: string = "";
|
||||||
|
dictationLastLen: number = 0;
|
||||||
|
hasGlobalStreams: boolean = false;
|
||||||
|
|
||||||
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
|
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
|
||||||
cacheManager: CacheManager;
|
cacheManager: CacheManager;
|
||||||
|
|
||||||
|
|
@ -90,6 +101,10 @@ export class ChatPanel {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSettings(newPluginInstance: LLMAgentPlugin) {
|
||||||
|
this.props.plugin = newPluginInstance;
|
||||||
|
}
|
||||||
|
|
||||||
updatePlaceholder(): void {
|
updatePlaceholder(): void {
|
||||||
const isEmpty = this.messageInput.textContent?.trim() === '';
|
const isEmpty = this.messageInput.textContent?.trim() === '';
|
||||||
const isFocused = document.activeElement === this.messageInput;
|
const isFocused = document.activeElement === this.messageInput;
|
||||||
|
|
@ -566,6 +581,10 @@ export class ChatPanel {
|
||||||
<button class="scroll-to-bottom-btn" id="scroll-to-bottom-btn" title="Прокрутить вниз" aria-label="Прокрутить к последнему сообщению">
|
<button class="scroll-to-bottom-btn" id="scroll-to-bottom-btn" title="Прокрутить вниз" aria-label="Прокрутить к последнему сообщению">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="global-stop-stream-btn" id="global-stop-stream-btn" style="display: none;" title="Отменить все генерации" aria-label="Остановить ответы LLM">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-history" id="chat-history"></div>
|
<div class="chat-history" id="chat-history"></div>
|
||||||
|
|
@ -610,6 +629,11 @@ export class ChatPanel {
|
||||||
<button class="stop-button" id="stop-button" style="display: none;">
|
<button class="stop-button" id="stop-button" style="display: none;">
|
||||||
<span class="stop-icon">■</span>
|
<span class="stop-icon">■</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="dictation-button" id="dictation-button" title="Извлечь голос (GM)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="22"></line></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="submit-button" id="send-button">
|
<button class="submit-button" id="send-button">
|
||||||
<span class="submit-text">⏎ Enter</span>
|
<span class="submit-text">⏎ Enter</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -638,6 +662,9 @@ export class ChatPanel {
|
||||||
this.maxTokensInput = this.container.querySelector('#max-tokens-input') as HTMLInputElement;
|
this.maxTokensInput = this.container.querySelector('#max-tokens-input') as HTMLInputElement;
|
||||||
this.maxTokensResetBtn = this.container.querySelector('#max-tokens-reset') as HTMLButtonElement;
|
this.maxTokensResetBtn = this.container.querySelector('#max-tokens-reset') as HTMLButtonElement;
|
||||||
|
|
||||||
|
this.globalStopBtn = this.container.querySelector('#global-stop-stream-btn') as HTMLButtonElement;
|
||||||
|
this.dictationBtn = this.container.querySelector('#dictation-button') as HTMLButtonElement;
|
||||||
|
|
||||||
const defaultTemp = (this.props.plugin.settings as any).temperatureChat ?? LLM_DEFAULTS.TEMPERATURE;
|
const defaultTemp = (this.props.plugin.settings as any).temperatureChat ?? LLM_DEFAULTS.TEMPERATURE;
|
||||||
this.tempInput.value = defaultTemp.toString();
|
this.tempInput.value = defaultTemp.toString();
|
||||||
|
|
||||||
|
|
@ -726,6 +753,19 @@ export class ChatPanel {
|
||||||
this.tempInput.value = def.toString();
|
this.tempInput.value = def.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Под микроФон
|
||||||
|
this.dictationBtn.addEventListener('click', () => this.toggleDictation());
|
||||||
|
// Под глобальный стоп
|
||||||
|
this.globalStopBtn.addEventListener('click', () => {
|
||||||
|
this.props.plugin.eventBus.emit('stop-all-streams');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подписка на изменение стримов
|
||||||
|
this.props.plugin.eventBus.on('global-stream-state-changed', (isStreaming: boolean) => {
|
||||||
|
this.hasGlobalStreams = isStreaming;
|
||||||
|
this.updateScrollButtonsVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
|
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
|
||||||
|
|
@ -1127,7 +1167,7 @@ export class ChatPanel {
|
||||||
// Обновить метод handleSend() для совместимости:
|
// Обновить метод handleSend() для совместимости:
|
||||||
handleSend(): void {
|
handleSend(): void {
|
||||||
const message = this.messageInput.textContent?.trim() || '';
|
const message = this.messageInput.textContent?.trim() || '';
|
||||||
if (message && this.props.onSendMessage) {
|
if ((message || this.currentAttachments.length > 0) && this.props.onSendMessage) {
|
||||||
const valTokens = this.maxTokensInput?.value;
|
const valTokens = this.maxTokensInput?.value;
|
||||||
const parsedMaxTokens = valTokens ? parseInt(valTokens, 10) : undefined;
|
const parsedMaxTokens = valTokens ? parseInt(valTokens, 10) : undefined;
|
||||||
|
|
||||||
|
|
@ -1515,13 +1555,160 @@ export class ChatPanel {
|
||||||
private updateScrollButtonsVisibility(): void {
|
private updateScrollButtonsVisibility(): void {
|
||||||
if (!this.cachedScrollContainer || !this.cachedBtnWrapper) return;
|
if (!this.cachedScrollContainer || !this.cachedBtnWrapper) return;
|
||||||
|
|
||||||
// DOM больше не сканируется через closest/querySelector, элементы закэшированы
|
|
||||||
const hasScrollbar = this.cachedScrollContainer.scrollHeight > (this.cachedScrollContainer.clientHeight + 2);
|
const hasScrollbar = this.cachedScrollContainer.scrollHeight > (this.cachedScrollContainer.clientHeight + 2);
|
||||||
|
|
||||||
if (hasScrollbar) {
|
// Управляем видимостью кнопок скролла (скрываем их индивидуально)
|
||||||
|
this.scrollToTopBtn.style.display = hasScrollbar ? 'flex' : 'none';
|
||||||
|
this.scrollToBottomBtn.style.display = hasScrollbar ? 'flex' : 'none';
|
||||||
|
this.globalStopBtn.style.display = this.hasGlobalStreams ? 'flex' : 'none';
|
||||||
|
|
||||||
|
// Обертка видна, если есть либо скроллбар, либо активный стрим
|
||||||
|
if (hasScrollbar || this.hasGlobalStreams) {
|
||||||
this.cachedBtnWrapper.classList.add('is-visible');
|
this.cachedBtnWrapper.classList.add('is-visible');
|
||||||
} else {
|
} else {
|
||||||
this.cachedBtnWrapper.classList.remove('is-visible');
|
this.cachedBtnWrapper.classList.remove('is-visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async toggleDictation() {
|
||||||
|
if (this.isDictating) {
|
||||||
|
await this.stopDictation();
|
||||||
|
} else {
|
||||||
|
await this.startDictation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------- Dictation ---------------------------------------------------------------
|
||||||
|
|
||||||
|
private async startDictation() {
|
||||||
|
this.isDictating = true;
|
||||||
|
this.dictationBtn.classList.add('pulsing');
|
||||||
|
this.dictationBuffer = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const action = this.props.plugin.settings.dictationStartAction || 'start';
|
||||||
|
const stRes = await fetch(`${this.props.plugin.settings.apiBaseUrl}/voice/status`);
|
||||||
|
const stData = await stRes.json();
|
||||||
|
|
||||||
|
if (action === 'start') {
|
||||||
|
// Если требуется чистый старт, а запись уже идет — сначала жестко глушим её
|
||||||
|
if (stData.is_running) {
|
||||||
|
await fetch(`${this.props.plugin.settings.apiBaseUrl}/voice/control`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: 'stop' })
|
||||||
|
});
|
||||||
|
// Даем бэкенду полсекунды на корректное закрытие аудио-потоков
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем чистую сессию (создаст новый файл лога на бэкенде)
|
||||||
|
await fetch(`${this.props.plugin.settings.apiBaseUrl}/voice/control`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: 'start' })
|
||||||
|
});
|
||||||
|
new Notice('Новая сессия диктовки запущена', 2000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Логика для 'continue'
|
||||||
|
if (!stData.is_running) {
|
||||||
|
// Запускаем и просим бэкенд продолжить писать в последний файл
|
||||||
|
await fetch(`${this.props.plugin.settings.apiBaseUrl}/voice/control`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: 'continue' })
|
||||||
|
});
|
||||||
|
new Notice('Служба диктовки возобновлена', 2000);
|
||||||
|
}
|
||||||
|
// Если уже is_running === true, ничего не отправляем, просто цепляемся к потоку
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все команды отправлены. Запрашиваем актуальный статус,
|
||||||
|
// чтобы получить ID нового/текущего файла и стартовую длину текста
|
||||||
|
const newStRes = await fetch(`${this.props.plugin.settings.apiBaseUrl}/voice/status`);
|
||||||
|
const newStData = await newStRes.json();
|
||||||
|
|
||||||
|
this.dictationSessionId = newStData.session_id;
|
||||||
|
this.dictationLastLen = newStData.pure_gm_text?.length || 0;
|
||||||
|
|
||||||
|
// Запускаем интервал сбора новых слов
|
||||||
|
this.dictationPollTimer = window.setInterval(async () => {
|
||||||
|
await this.pollDictationText();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Ошибка старта диктовки: ", e);
|
||||||
|
this.stopDictation();
|
||||||
|
new Notice('Ошибка подключения к службе транскрибации');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pollDictationText() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${this.props.plugin.settings.apiBaseUrl}/voice/status`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Если сессия сбросилась (файл лога другой) - обнуляем длину
|
||||||
|
if (data.session_id !== this.dictationSessionId) {
|
||||||
|
this.dictationSessionId = data.session_id;
|
||||||
|
this.dictationLastLen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = data.pure_gm_text || "";
|
||||||
|
if (text.length > this.dictationLastLen) {
|
||||||
|
// Отрезаем новый кусок
|
||||||
|
const newWords = text.substring(this.dictationLastLen).trim();
|
||||||
|
if (newWords) {
|
||||||
|
this.dictationBuffer += (this.dictationBuffer ? " " : "") + newWords;
|
||||||
|
}
|
||||||
|
this.dictationLastLen = text.length;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем фоновые ошибки сети
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async stopDictation() {
|
||||||
|
this.isDictating = false;
|
||||||
|
this.dictationBtn.classList.remove('pulsing');
|
||||||
|
|
||||||
|
if (this.dictationPollTimer) {
|
||||||
|
clearInterval(this.dictationPollTimer);
|
||||||
|
this.dictationPollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dictationBuffer.trim().length > 0) {
|
||||||
|
try {
|
||||||
|
// Кешируем как md файл
|
||||||
|
const cacheResult = await this.cacheManager.cacheTextData(
|
||||||
|
this.dictationBuffer.trim(),
|
||||||
|
`Голосовая_заметка` // Это пойдет в имя
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const timeStr = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`;
|
||||||
|
|
||||||
|
// Формируем Attachment (Используем тип url, или external - кеш менеджер сделает md файл)
|
||||||
|
const newAttachment: Attachment = {
|
||||||
|
type: 'external',
|
||||||
|
name: `dictation-${timeStr}.md`,
|
||||||
|
sourcePath: `virtual-dictation-${timeStr}`,
|
||||||
|
cachedPath: cacheResult.cachedPath,
|
||||||
|
uuid: cacheResult.uuid,
|
||||||
|
permanent: false,
|
||||||
|
mimeType: cacheResult.mimeType
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentAttachments.push(newAttachment);
|
||||||
|
this.renderAttachedFiles();
|
||||||
|
new Notice('Диктовка прикреплена');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Ошибка сохранения диктовки:", e);
|
||||||
|
new Notice('Не удалось сохранить файл диктовки');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new Notice('Записан пустой текст, вложение отменено');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dictationBuffer = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1585,4 +1585,78 @@
|
||||||
.param-reset-btn:hover {
|
.param-reset-btn:hover {
|
||||||
color: var(--text-normal) !important;
|
color: var(--text-normal) !important;
|
||||||
background-color: var(--background-modifier-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------- Stop button and dictation --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Новая глобальная кнопка отмены генерации (внутри wrapper) */
|
||||||
|
.global-stop-stream-btn {
|
||||||
|
pointer-events: auto;
|
||||||
|
border: 2px solid var(--text-error); /* Красная окантовка для опасности */
|
||||||
|
border-radius: 8px; /* Квадратная форма с закруглением вместо 50% */
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
background-color: var(--background-modifier-error); /* Фон красного толка */
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-stop-stream-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
background-color: var(--text-error) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-stop-stream-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-stop-stream-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
stroke-width: 3px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопка диктовки */
|
||||||
|
.dictation-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s, color 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dictation-button:hover {
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимация пульсации при записи */
|
||||||
|
.dictation-button.pulsing {
|
||||||
|
animation: dictation-pulse 1.5s infinite;
|
||||||
|
color: var(--text-error); /* Красный цвет записи */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dictation-pulse {
|
||||||
|
0% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.2); opacity: 0.6; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,11 @@ export class ChatView extends ItemView {
|
||||||
handleSettingsChanged(): void {
|
handleSettingsChanged(): void {
|
||||||
if (this.chatPanel)
|
if (this.chatPanel)
|
||||||
{
|
{
|
||||||
|
// 1. Обновляем модель (это у вас уже было)
|
||||||
this.chatPanel.setSelectedModel(this.plugin.settings.defaultModel);
|
this.chatPanel.setSelectedModel(this.plugin.settings.defaultModel);
|
||||||
|
|
||||||
|
// 2. Уведомляем панель об обновлении всего объекта плагина (и настроек внутри)
|
||||||
|
this.chatPanel.updateSettings(this.plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user