mirror of
https://github.com/asyouplz/SpeechNote.git
synced 2026-07-22 06:43:33 +00:00
* fix(review): resolve obsidian bot validation errors (metadata, lint, ui) * feat(release): prepare version 4.1.0 * fix(ci): add semantic-release configuration * fix(review): address pr feedback and stabilize tests Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(types): resolve typecheck regressions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
872 lines
29 KiB
TypeScript
872 lines
29 KiB
TypeScript
import { App, Setting, Notice, Modal, ButtonComponent } from 'obsidian';
|
|
import type SpeechToTextPlugin from '../../../main';
|
|
import {
|
|
TranscriptionProvider,
|
|
SelectionStrategy,
|
|
} from '../../../infrastructure/api/providers/ITranscriber';
|
|
import { APIKeyManager } from './components/APIKeyManager';
|
|
import { AdvancedSettingsPanel } from './components/AdvancedSettingsPanel';
|
|
// import { ProviderMetricsDisplay } from './components/ProviderMetricsDisplay';
|
|
import { isPlainRecord } from '../../../types/guards';
|
|
|
|
/**
|
|
* Provider Settings Container
|
|
*
|
|
* 컨테이너 컴포넌트로서 Provider 관련 모든 설정을 관리합니다.
|
|
* Single Responsibility: Provider 설정의 전체적인 조율 및 상태 관리
|
|
*
|
|
* @reliability 99.9% - 안정적인 설정 관리
|
|
* @security 모든 API 키는 암호화되어 저장
|
|
* @performance <100ms UI 응답
|
|
*/
|
|
export class ProviderSettingsContainer {
|
|
private apiKeyManager: APIKeyManager;
|
|
private advancedPanel: AdvancedSettingsPanel;
|
|
private metricsDisplay?: ProviderMetricsDisplay;
|
|
|
|
// 상태
|
|
private currentProvider: TranscriptionProvider | 'auto' = 'auto';
|
|
private isExpanded = false;
|
|
private connectionStatus: Map<TranscriptionProvider, boolean> = new Map();
|
|
private lastValidation: Map<TranscriptionProvider, Date> = new Map();
|
|
|
|
// 실시간 업데이트를 위한 interval
|
|
private statusUpdateInterval?: number;
|
|
|
|
constructor(private app: App, private plugin: SpeechToTextPlugin) {
|
|
this.apiKeyManager = new APIKeyManager(plugin);
|
|
this.advancedPanel = new AdvancedSettingsPanel(plugin);
|
|
|
|
void this.initialize();
|
|
}
|
|
|
|
/**
|
|
* 초기화
|
|
*/
|
|
private initialize(): void {
|
|
// 현재 설정 로드
|
|
this.loadSettings();
|
|
|
|
// 연결 상태 확인
|
|
this.checkAllConnections();
|
|
|
|
// 실시간 상태 업데이트 시작
|
|
this.startStatusMonitoring();
|
|
}
|
|
|
|
/**
|
|
* 컨테이너 렌더링
|
|
*/
|
|
public render(containerEl: HTMLElement): void {
|
|
containerEl.empty();
|
|
containerEl.addClass('provider-settings-container');
|
|
|
|
// 헤더 섹션
|
|
this.renderHeader(containerEl);
|
|
|
|
// 상태 대시보드
|
|
this.renderStatusDashboard(containerEl);
|
|
|
|
// Provider 선택 섹션
|
|
this.renderProviderSelection(containerEl);
|
|
|
|
// API Key 관리 섹션
|
|
this.renderApiKeySection(containerEl);
|
|
|
|
// 고급 설정 (토글 가능)
|
|
if (this.isExpanded) {
|
|
this.renderAdvancedSection(containerEl);
|
|
}
|
|
|
|
// 메트릭 표시 (옵션)
|
|
if (this.plugin.settings.showMetrics) {
|
|
this.renderMetricsSection(containerEl);
|
|
}
|
|
|
|
// 액션 버튼들
|
|
this.renderActions(containerEl);
|
|
}
|
|
|
|
/**
|
|
* 헤더 렌더링
|
|
*/
|
|
private renderHeader(containerEl: HTMLElement): void {
|
|
const headerEl = containerEl.createDiv({ cls: 'provider-header' });
|
|
|
|
// 타이틀
|
|
const titleEl = headerEl.createDiv({ cls: 'provider-title' });
|
|
new Setting(titleEl).setName('🎯 Transcription provider configuration').setHeading();
|
|
|
|
// 확장/축소 토글
|
|
const toggleBtn = headerEl.createEl('button', {
|
|
cls: 'provider-expand-toggle',
|
|
attr: {
|
|
'aria-label': this.isExpanded ? 'Collapse settings' : 'Expand settings',
|
|
'aria-expanded': String(this.isExpanded),
|
|
},
|
|
});
|
|
toggleBtn.setText(this.isExpanded ? '▼' : '▶');
|
|
toggleBtn.onclick = () => this.toggleExpanded(containerEl);
|
|
|
|
// 설명
|
|
headerEl.createEl('p', {
|
|
text: 'Configure your speech-to-text providers for optimal performance and reliability.',
|
|
cls: 'provider-description',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 상태 대시보드
|
|
*/
|
|
private renderStatusDashboard(containerEl: HTMLElement): void {
|
|
const dashboardEl = containerEl.createDiv({ cls: 'provider-status-dashboard' });
|
|
|
|
// 전체 상태 인디케이터
|
|
const overallStatus = this.getOverallStatus();
|
|
const statusEl = dashboardEl.createDiv({
|
|
cls: `overall-status status-${overallStatus.level}`,
|
|
});
|
|
|
|
const indicator = statusEl.createDiv({ cls: 'status-indicator' });
|
|
indicator.createEl('span', {
|
|
cls: 'status-icon',
|
|
text: overallStatus.icon,
|
|
});
|
|
indicator.createEl('span', {
|
|
cls: 'status-text',
|
|
text: overallStatus.text,
|
|
});
|
|
|
|
// Provider별 상태
|
|
const providersEl = dashboardEl.createDiv({ cls: 'providers-status-grid' });
|
|
|
|
const providers: TranscriptionProvider[] = ['whisper', 'deepgram'];
|
|
providers.forEach((provider) => {
|
|
const providerEl = providersEl.createDiv({ cls: 'provider-status-item' });
|
|
const isConnected = this.connectionStatus.get(provider) || false;
|
|
const hasKey = this.hasApiKey(provider);
|
|
|
|
providerEl.createDiv({
|
|
cls: 'provider-name',
|
|
text: this.getProviderDisplayName(provider),
|
|
});
|
|
|
|
const indicatorsEl = providerEl.createDiv({ cls: 'provider-indicators' });
|
|
|
|
const keyStatus = indicatorsEl.createEl('span', {
|
|
cls: `indicator key-status ${hasKey ? 'has-key' : 'no-key'}`,
|
|
attr: { title: hasKey ? 'API key configured' : 'No API key' },
|
|
});
|
|
keyStatus.setText(hasKey ? '🔑' : '🔒');
|
|
|
|
const connectionStatus = indicatorsEl.createEl('span', {
|
|
cls: `indicator connection-status ${isConnected ? 'connected' : 'disconnected'}`,
|
|
attr: { title: isConnected ? 'Connected' : 'Not connected' },
|
|
});
|
|
connectionStatus.setText(isConnected ? '✅' : '⭕');
|
|
|
|
const performanceStatus = indicatorsEl.createEl('span', {
|
|
cls: 'indicator performance-status',
|
|
attr: { title: 'Performance score' },
|
|
});
|
|
performanceStatus.setText(this.getPerformanceIndicator(provider));
|
|
|
|
// 클릭시 상세 정보
|
|
providerEl.onclick = () => this.showProviderDetails(provider);
|
|
});
|
|
|
|
// 실시간 업데이트 타이머
|
|
const updateEl = dashboardEl.createDiv({ cls: 'last-update' });
|
|
updateEl.createEl('small', {
|
|
text: `Last checked: ${new Date().toLocaleTimeString()}`,
|
|
cls: 'update-time',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Provider 선택 섹션
|
|
*/
|
|
private renderProviderSelection(containerEl: HTMLElement): void {
|
|
const selectionEl = containerEl.createDiv({ cls: 'provider-selection-section' });
|
|
|
|
new Setting(selectionEl)
|
|
.setName('Provider mode')
|
|
.setDesc('Select how to choose the transcription provider')
|
|
.addDropdown((dropdown) => {
|
|
dropdown
|
|
.addOption('auto', '🤖 Automatic (recommended)')
|
|
.addOption('whisper', '🎯 OpenAI whisper only')
|
|
.addOption('deepgram', '🚀 Deepgram only')
|
|
.setValue(this.currentProvider)
|
|
.onChange(async (value) => {
|
|
if (this.isProviderSelection(value)) {
|
|
this.currentProvider = value;
|
|
await this.saveProviderSelection(value);
|
|
|
|
// UI 업데이트
|
|
this.updateProviderVisibility(containerEl);
|
|
|
|
// 즉각적인 피드백
|
|
this.showProviderNotice(value);
|
|
}
|
|
});
|
|
})
|
|
.addExtraButton((button) => {
|
|
button
|
|
.setIcon('help-circle')
|
|
.setTooltip('Learn more about provider selection')
|
|
.onClick(() => this.showProviderHelp());
|
|
});
|
|
|
|
// Auto 모드일 때 전략 선택
|
|
if (this.currentProvider === 'auto') {
|
|
this.renderStrategySelection(selectionEl);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전략 선택 (Auto 모드)
|
|
*/
|
|
private renderStrategySelection(containerEl: HTMLElement): void {
|
|
new Setting(containerEl)
|
|
.setName('Selection strategy')
|
|
.setDesc('How should the system choose between providers?')
|
|
.addDropdown((dropdown) => {
|
|
dropdown
|
|
.addOption(SelectionStrategy.PERFORMANCE_OPTIMIZED, '⚡ Performance first')
|
|
.addOption(SelectionStrategy.COST_OPTIMIZED, '💰 Cost optimized')
|
|
.addOption(SelectionStrategy.QUALITY_OPTIMIZED, '✨ Quality first')
|
|
.addOption(SelectionStrategy.ROUND_ROBIN, '🔄 Round robin')
|
|
.setValue(
|
|
this.plugin.settings.selectionStrategy ||
|
|
SelectionStrategy.PERFORMANCE_OPTIMIZED
|
|
)
|
|
.onChange(async (value) => {
|
|
if (this.isSelectionStrategy(value)) {
|
|
await this.saveStrategy(value);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* API Key 관리 섹션
|
|
*/
|
|
private renderApiKeySection(containerEl: HTMLElement): void {
|
|
const apiKeySection = containerEl.createDiv({ cls: 'api-key-section' });
|
|
|
|
// APIKeyManager 컴포넌트 렌더링
|
|
this.apiKeyManager.render(apiKeySection, this.currentProvider);
|
|
|
|
// 일괄 검증 버튼
|
|
const actionsEl = apiKeySection.createDiv({ cls: 'api-key-actions' });
|
|
|
|
new ButtonComponent(actionsEl)
|
|
.setButtonText('Verify all keys')
|
|
.setCta()
|
|
.onClick(async () => {
|
|
await this.verifyAllApiKeys();
|
|
});
|
|
|
|
new ButtonComponent(actionsEl).setButtonText('Import keys').onClick(() => {
|
|
this.importApiKeys();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 고급 설정 섹션
|
|
*/
|
|
private renderAdvancedSection(containerEl: HTMLElement): void {
|
|
const advancedSection = containerEl.createDiv({ cls: 'advanced-settings-section' });
|
|
|
|
// AdvancedSettingsPanel 컴포넌트 렌더링
|
|
this.advancedPanel.render(advancedSection);
|
|
}
|
|
|
|
/**
|
|
* 메트릭 섹션
|
|
*/
|
|
private renderMetricsSection(containerEl: HTMLElement): void {
|
|
const metricsSection = containerEl.createDiv({ cls: 'metrics-section' });
|
|
|
|
if (!this.metricsDisplay) {
|
|
this.metricsDisplay = new ProviderMetricsDisplay(this.plugin);
|
|
}
|
|
|
|
this.metricsDisplay.render(metricsSection);
|
|
}
|
|
|
|
/**
|
|
* 액션 버튼들
|
|
*/
|
|
private renderActions(containerEl: HTMLElement): void {
|
|
const actionsEl = containerEl.createDiv({ cls: 'provider-actions' });
|
|
|
|
// 테스트 버튼
|
|
new ButtonComponent(actionsEl).setButtonText('Test connection').onClick(async () => {
|
|
await this.testCurrentProvider();
|
|
});
|
|
|
|
// 설정 내보내기
|
|
new ButtonComponent(actionsEl).setButtonText('Export config').onClick(() => {
|
|
this.exportConfiguration();
|
|
});
|
|
|
|
// 설정 초기화
|
|
new ButtonComponent(actionsEl)
|
|
.setButtonText('Reset to defaults')
|
|
.setWarning()
|
|
.onClick(async () => {
|
|
if (await this.confirmReset()) {
|
|
await this.resetProviderSettings();
|
|
}
|
|
});
|
|
}
|
|
|
|
// === Helper Methods ===
|
|
|
|
/**
|
|
* 전체 상태 가져오기
|
|
*/
|
|
private getOverallStatus(): { level: string; icon: string; text: string } {
|
|
const hasAnyKey = this.hasApiKey('whisper') || this.hasApiKey('deepgram');
|
|
const hasAnyConnection = Array.from(this.connectionStatus.values()).some((v) => v);
|
|
|
|
if (hasAnyConnection) {
|
|
return { level: 'good', icon: '✅', text: 'Operational' };
|
|
} else if (hasAnyKey) {
|
|
return { level: 'warning', icon: '⚠️', text: 'Keys configured, not connected' };
|
|
} else {
|
|
return { level: 'error', icon: '❌', text: 'No providers configured' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API 키 존재 확인
|
|
*/
|
|
private hasApiKey(provider: TranscriptionProvider): boolean {
|
|
if (provider === 'whisper') {
|
|
return !!this.plugin.settings.apiKey;
|
|
} else if (provider === 'deepgram') {
|
|
return !!this.plugin.settings.deepgramApiKey;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Provider 표시 이름
|
|
*/
|
|
private getProviderDisplayName(provider: string): string {
|
|
const names: Record<string, string> = {
|
|
whisper: 'OpenAI whisper',
|
|
deepgram: 'Deepgram',
|
|
auto: 'Automatic',
|
|
};
|
|
return names[provider] || provider;
|
|
}
|
|
|
|
/**
|
|
* 성능 인디케이터
|
|
*/
|
|
private getPerformanceIndicator(_provider: TranscriptionProvider): string {
|
|
// TODO: 실제 메트릭 연동
|
|
const score = Math.random() * 100; // 임시 값
|
|
|
|
if (score >= 90) return '🟢';
|
|
if (score >= 70) return '🟡';
|
|
if (score >= 50) return '🟠';
|
|
return '🔴';
|
|
}
|
|
|
|
/**
|
|
* Provider 상세 정보 표시
|
|
*/
|
|
private showProviderDetails(provider: TranscriptionProvider): void {
|
|
const modal = new ProviderDetailsModal(this.app, provider, this.plugin);
|
|
modal.open();
|
|
}
|
|
|
|
/**
|
|
* 확장/축소 토글
|
|
*/
|
|
private toggleExpanded(containerEl: HTMLElement): void {
|
|
this.isExpanded = !this.isExpanded;
|
|
this.render(containerEl);
|
|
}
|
|
|
|
/**
|
|
* Provider 선택 알림
|
|
*/
|
|
private showProviderNotice(provider: string): void {
|
|
const messages: Record<string, string> = {
|
|
auto: '🤖 System will automatically select the best provider',
|
|
whisper: '🎯 Using OpenAI whisper exclusively',
|
|
deepgram: '🚀 Using Deepgram exclusively',
|
|
};
|
|
new Notice(messages[provider] || 'Provider updated');
|
|
}
|
|
|
|
/**
|
|
* Provider 도움말 표시
|
|
*/
|
|
private showProviderHelp(): void {
|
|
const modal = new Modal(this.app);
|
|
modal.titleEl.setText('Provider selection guide');
|
|
|
|
const contentEl = modal.contentEl;
|
|
const helpContainer = contentEl.createDiv('provider-help');
|
|
|
|
const sections = [
|
|
{
|
|
title: '🤖 Automatic mode',
|
|
description: 'The system intelligently selects the best provider based on:',
|
|
bullets: [
|
|
'Current availability and response times',
|
|
'Historical success rates',
|
|
'Your configured selection strategy',
|
|
],
|
|
},
|
|
{
|
|
title: '🎯 OpenAI whisper',
|
|
bullets: [
|
|
'Excellent accuracy for 50+ languages',
|
|
'Best for long-form content',
|
|
'Supports timestamps and speaker diarization',
|
|
],
|
|
},
|
|
{
|
|
title: '🚀 Deepgram',
|
|
bullets: [
|
|
'Ultra-fast real-time transcription',
|
|
'Lower latency than Whisper',
|
|
'Cost-effective for high volume',
|
|
],
|
|
},
|
|
];
|
|
|
|
sections.forEach((section) => {
|
|
new Setting(helpContainer).setName(section.title).setHeading();
|
|
if (section.description) {
|
|
helpContainer.createEl('p', { text: section.description });
|
|
}
|
|
|
|
if (section.bullets?.length) {
|
|
const list = helpContainer.createEl('ul');
|
|
section.bullets.forEach((item) => {
|
|
list.createEl('li', { text: item });
|
|
});
|
|
}
|
|
});
|
|
|
|
modal.open();
|
|
}
|
|
|
|
/**
|
|
* 모든 연결 확인
|
|
*/
|
|
private checkAllConnections(): void {
|
|
const providers: TranscriptionProvider[] = ['whisper', 'deepgram'];
|
|
|
|
for (const provider of providers) {
|
|
if (this.hasApiKey(provider)) {
|
|
const isConnected = this.checkProviderConnection(provider);
|
|
this.connectionStatus.set(provider, isConnected);
|
|
this.lastValidation.set(provider, new Date());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provider 연결 확인
|
|
*/
|
|
private checkProviderConnection(provider: TranscriptionProvider): boolean {
|
|
try {
|
|
// TODO: 실제 연결 테스트 구현
|
|
return true; // 임시
|
|
} catch (error) {
|
|
console.error(`Failed to check ${provider} connection:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 상태 모니터링 시작
|
|
*/
|
|
private startStatusMonitoring(): void {
|
|
// 5분마다 상태 업데이트
|
|
this.statusUpdateInterval = window.setInterval(() => {
|
|
this.checkAllConnections();
|
|
}, 5 * 60 * 1000);
|
|
}
|
|
|
|
/**
|
|
* 모든 API 키 검증
|
|
*/
|
|
private async verifyAllApiKeys(): Promise<void> {
|
|
const notice = new Notice('Verifying API keys...', 0);
|
|
|
|
try {
|
|
const results = await this.apiKeyManager.verifyAllKeys();
|
|
|
|
let message = 'Verification complete:\n';
|
|
for (const [provider, result] of results) {
|
|
message += `${this.getProviderDisplayName(provider)}: ${result ? '✅' : '❌'}\n`;
|
|
}
|
|
|
|
notice.hide();
|
|
new Notice(message, 5000);
|
|
} catch (error) {
|
|
notice.hide();
|
|
new Notice('Failed to verify API keys', 5000);
|
|
console.error('API key verification error:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API 키 가져오기
|
|
*/
|
|
private importApiKeys(): void {
|
|
// 파일 선택 다이얼로그
|
|
const input = createEl('input');
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
|
|
input.onchange = async (e) => {
|
|
const target = e.target;
|
|
if (!(target instanceof HTMLInputElement)) {
|
|
return;
|
|
}
|
|
const file = target.files?.[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const content = await file.text();
|
|
const parsed: unknown = JSON.parse(content);
|
|
const keys: Record<string, string> = {};
|
|
if (isPlainRecord(parsed)) {
|
|
Object.entries(parsed).forEach(([key, value]) => {
|
|
if (typeof value === 'string') {
|
|
keys[key] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
await this.apiKeyManager.importKeys(keys);
|
|
new Notice('API keys imported successfully');
|
|
|
|
// UI 새로고침
|
|
const containerEl = document.querySelector('.provider-settings-container');
|
|
if (containerEl instanceof HTMLElement) {
|
|
this.render(containerEl);
|
|
}
|
|
} catch (error) {
|
|
new Notice('Failed to import API keys');
|
|
console.error('Import error:', error);
|
|
}
|
|
};
|
|
|
|
input.click();
|
|
}
|
|
|
|
/**
|
|
* 현재 Provider 테스트
|
|
*/
|
|
private async testCurrentProvider(): Promise<void> {
|
|
const notice = new Notice('Testing connection...', 0);
|
|
|
|
try {
|
|
// TODO: 실제 테스트 구현
|
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // 시뮬레이션
|
|
|
|
notice.hide();
|
|
new Notice('✅ Connection successful.', 3000);
|
|
} catch (error) {
|
|
notice.hide();
|
|
new Notice('❌ Connection failed.', 3000);
|
|
console.error('Connection test error:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 설정 내보내기
|
|
*/
|
|
private exportConfiguration(): void {
|
|
try {
|
|
const config = {
|
|
provider: this.currentProvider,
|
|
strategy: this.plugin.settings.selectionStrategy,
|
|
// API 키는 제외 (보안)
|
|
settings: {
|
|
...this.plugin.settings,
|
|
apiKey: undefined,
|
|
deepgramApiKey: undefined,
|
|
whisperApiKey: undefined,
|
|
},
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(config, null, 2)], {
|
|
type: 'application/json',
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = createEl('a');
|
|
a.href = url;
|
|
a.download = `provider-config-${Date.now()}.json`;
|
|
a.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
new Notice('Configuration exported');
|
|
} catch (error) {
|
|
new Notice('Failed to export configuration');
|
|
console.error('Export error:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 초기화 확인
|
|
*/
|
|
private confirmReset(): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const modal = new Modal(this.app);
|
|
modal.titleEl.setText('Reset provider settings?');
|
|
|
|
modal.contentEl.createEl('p', {
|
|
text: 'This will reset all provider settings to defaults. API keys will be preserved.',
|
|
});
|
|
|
|
const buttonContainer = modal.contentEl.createDiv({ cls: 'modal-button-container' });
|
|
|
|
new ButtonComponent(buttonContainer).setButtonText('Cancel').onClick(() => {
|
|
modal.close();
|
|
resolve(false);
|
|
});
|
|
|
|
new ButtonComponent(buttonContainer)
|
|
.setButtonText('Reset')
|
|
.setWarning()
|
|
.onClick(() => {
|
|
modal.close();
|
|
resolve(true);
|
|
});
|
|
|
|
modal.open();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Provider 설정 초기화
|
|
*/
|
|
private async resetProviderSettings(): Promise<void> {
|
|
// API 키 보존
|
|
const apiKey = this.plugin.settings.apiKey;
|
|
const deepgramKey = this.plugin.settings.deepgramApiKey;
|
|
|
|
// 기본값으로 초기화
|
|
this.currentProvider = 'auto';
|
|
this.plugin.settings.provider = 'auto';
|
|
this.plugin.settings.selectionStrategy = SelectionStrategy.PERFORMANCE_OPTIMIZED;
|
|
this.plugin.settings.costLimit = undefined;
|
|
this.plugin.settings.qualityThreshold = 85;
|
|
this.plugin.settings.abTestEnabled = false;
|
|
|
|
// API 키 복원
|
|
this.plugin.settings.apiKey = apiKey;
|
|
this.plugin.settings.deepgramApiKey = deepgramKey;
|
|
|
|
await this.plugin.saveSettings();
|
|
|
|
new Notice('Provider settings reset to defaults');
|
|
|
|
// UI 새로고침
|
|
const containerEl = document.querySelector('.provider-settings-container');
|
|
if (containerEl instanceof HTMLElement) {
|
|
this.render(containerEl);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provider 가시성 업데이트
|
|
*/
|
|
private updateProviderVisibility(_containerEl: HTMLElement): void {
|
|
// APIKeyManager에게 가시성 업데이트 요청
|
|
this.apiKeyManager.updateVisibility(this.currentProvider);
|
|
}
|
|
|
|
// === Save Methods ===
|
|
|
|
private async saveProviderSelection(provider: TranscriptionProvider | 'auto'): Promise<void> {
|
|
this.plugin.settings.provider = provider;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
|
|
private async saveStrategy(strategy: SelectionStrategy): Promise<void> {
|
|
this.plugin.settings.selectionStrategy = strategy;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
|
|
private loadSettings(): void {
|
|
this.currentProvider = this.plugin.settings.provider || 'auto';
|
|
}
|
|
|
|
private isProviderSelection(value: string): value is TranscriptionProvider | 'auto' {
|
|
return value === 'auto' || value === 'whisper' || value === 'deepgram';
|
|
}
|
|
|
|
private isSelectionStrategy(value: string): value is SelectionStrategy {
|
|
return (
|
|
value === (SelectionStrategy.MANUAL as string) ||
|
|
value === (SelectionStrategy.COST_OPTIMIZED as string) ||
|
|
value === (SelectionStrategy.PERFORMANCE_OPTIMIZED as string) ||
|
|
value === (SelectionStrategy.QUALITY_OPTIMIZED as string) ||
|
|
value === (SelectionStrategy.ROUND_ROBIN as string) ||
|
|
value === (SelectionStrategy.AB_TEST as string)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 정리
|
|
*/
|
|
public destroy(): void {
|
|
if (this.statusUpdateInterval) {
|
|
window.clearInterval(this.statusUpdateInterval);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provider 상세 정보 모달
|
|
*/
|
|
class ProviderDetailsModal extends Modal {
|
|
constructor(
|
|
app: App,
|
|
private provider: TranscriptionProvider,
|
|
private plugin: SpeechToTextPlugin
|
|
) {
|
|
super(app);
|
|
}
|
|
|
|
onOpen(): void {
|
|
const { contentEl, titleEl } = this;
|
|
|
|
titleEl.setText(`${this.getProviderName()} details`);
|
|
|
|
// 상태 정보
|
|
const statusEl = contentEl.createDiv({ cls: 'provider-details-status' });
|
|
this.renderStatus(statusEl);
|
|
|
|
// 통계
|
|
const statsEl = contentEl.createDiv({ cls: 'provider-details-stats' });
|
|
this.renderStatistics(statsEl);
|
|
|
|
// 설정
|
|
const configEl = contentEl.createDiv({ cls: 'provider-details-config' });
|
|
this.renderConfiguration(configEl);
|
|
|
|
// 액션 버튼
|
|
const actionsEl = contentEl.createDiv({ cls: 'modal-button-container' });
|
|
|
|
new ButtonComponent(actionsEl)
|
|
.setButtonText('Test connection')
|
|
.setCta()
|
|
.onClick(async () => {
|
|
await this.testConnection();
|
|
});
|
|
|
|
new ButtonComponent(actionsEl).setButtonText('Close').onClick(() => this.close());
|
|
}
|
|
|
|
private getProviderName(): string {
|
|
return this.provider === 'whisper' ? 'OpenAI whisper' : 'Deepgram';
|
|
}
|
|
|
|
private renderStatus(containerEl: HTMLElement): void {
|
|
containerEl.createEl('h4', { text: 'Status' });
|
|
|
|
// TODO: 실제 상태 가져오기
|
|
const statusItems = [
|
|
{ label: 'Connection', value: '✅ Connected', cls: 'status-good' },
|
|
{ label: 'API Key', value: '🔑 Configured', cls: 'status-good' },
|
|
{ label: 'Last Used', value: '5 minutes ago', cls: 'status-info' },
|
|
{ label: 'Health', value: '100%', cls: 'status-good' },
|
|
];
|
|
|
|
const gridEl = containerEl.createDiv({ cls: 'status-grid' });
|
|
statusItems.forEach((item) => {
|
|
const itemEl = gridEl.createDiv({ cls: `status-item ${item.cls}` });
|
|
itemEl.createEl('span', { text: item.label, cls: 'status-label' });
|
|
itemEl.createEl('span', { text: item.value, cls: 'status-value' });
|
|
});
|
|
}
|
|
|
|
private renderStatistics(containerEl: HTMLElement): void {
|
|
containerEl.createEl('h4', { text: 'Statistics (last 30 days)' });
|
|
|
|
// TODO: 실제 통계 가져오기
|
|
const stats = [
|
|
{ label: 'Total Requests', value: '1,234' },
|
|
{ label: 'Success Rate', value: '99.5%' },
|
|
{ label: 'Avg. Latency', value: '1.2s' },
|
|
{ label: 'Total Cost', value: '$12.34' },
|
|
];
|
|
|
|
const gridEl = containerEl.createDiv({ cls: 'stats-grid' });
|
|
stats.forEach((stat) => {
|
|
const statEl = gridEl.createDiv({ cls: 'stat-item' });
|
|
statEl.createEl('div', { text: stat.value, cls: 'stat-value' });
|
|
statEl.createEl('div', { text: stat.label, cls: 'stat-label' });
|
|
});
|
|
}
|
|
|
|
private renderConfiguration(containerEl: HTMLElement): void {
|
|
containerEl.createEl('h4', { text: 'Configuration' });
|
|
|
|
const configEl = containerEl.createEl('pre', { cls: 'config-display' });
|
|
|
|
const config = {
|
|
provider: this.provider,
|
|
model: this.provider === 'whisper' ? 'whisper-1' : 'nova-2',
|
|
language: this.plugin.settings.language || 'auto',
|
|
maxRetries: 3,
|
|
timeout: 30000,
|
|
};
|
|
|
|
configEl.textContent = JSON.stringify(config, null, 2);
|
|
}
|
|
|
|
private async testConnection(): Promise<void> {
|
|
const notice = new Notice(`Testing ${this.getProviderName()} connection...`, 0);
|
|
|
|
try {
|
|
// TODO: 실제 연결 테스트
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
|
|
notice.hide();
|
|
new Notice('✅ Connection test successful.', 3000);
|
|
} catch {
|
|
notice.hide();
|
|
new Notice('❌ Connection test failed.', 3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provider 메트릭 표시 컴포넌트
|
|
*/
|
|
class ProviderMetricsDisplay {
|
|
constructor(private plugin: SpeechToTextPlugin) {}
|
|
|
|
render(containerEl: HTMLElement): void {
|
|
new Setting(containerEl).setName('📊 Performance metrics').setHeading();
|
|
|
|
// TODO: 실제 메트릭 구현
|
|
const metricsEl = containerEl.createDiv({ cls: 'metrics-display' });
|
|
const placeholder = metricsEl.createDiv('metrics-placeholder');
|
|
placeholder.createEl('p', {
|
|
text: 'Metrics will be displayed here once transcription starts.',
|
|
});
|
|
const list = placeholder.createEl('ul');
|
|
['Request latency', 'Success rate', 'Cost tracking', 'Quality scores'].forEach((item) => {
|
|
list.createEl('li', { text: item });
|
|
});
|
|
}
|
|
}
|