株主対話デモアプリケーションのシステムアーキテクチャについて、技術的な観点から詳細に説明します。本アプリケーションは、フロントエンドのみで完結するSPA(シングルページアプリケーション)として設計され、Azure OpenAI APIを活用した AI 駆動の対話生成システムです。
graph TB
subgraph "ユーザー端末 (ブラウザ)"
A[HTML5 UI Layer]
B[JavaScript Application Layer]
C[CSS Presentation Layer]
D[Browser Storage]
E[PDF.js Engine]
end
subgraph "外部サービス"
F[Azure OpenAI API]
G[CDN Services]
end
subgraph "外部リソース"
H[Tailwind CSS CDN]
I[PDF.js CDN]
end
A --> B
B --> C
B --> D
B --> E
B --> F
A --> G
G --> H
G --> I
B --> |API Calls| F
B --> |File Processing| E
B --> |Settings Storage| D
sequenceDiagram
participant U as User
participant UI as UI Layer
participant APP as Application Layer
participant API as Azure OpenAI
participant PDF as PDF.js
participant LS as Local Storage
U->>UI: PDFファイル選択
UI->>APP: ファイル受信
APP->>PDF: PDF解析実行
PDF->>APP: テキスト抽出結果
U->>UI: 対話開始ボタン
UI->>APP: 対話開始要求
APP->>LS: 設定情報取得
APP->>API: 質問生成要求
API->>APP: 株主質問
APP->>UI: 質問表示
APP->>API: 回答生成要求
API->>APP: 取締役回答
APP->>UI: 回答表示
Note over APP,API: 5ターン繰り返し
APP->>API: 要約生成要求
API->>APP: 対話要約
APP->>UI: 要約ポップアップ表示
HTML5:
- セマンティックマークアップ
- form要素による入力管理
- canvas要素でのPDFレンダリング
- modal要素での対話拡張表示
CSS3:
- Tailwind CSS(ユーティリティファースト)
- カスタムCSS(コンポーネント固有スタイル)
- CSS Grid & Flexbox(レイアウト)
- CSS Animations(UXエンハンス)
src/
├── index.html # メインHTMLドキュメント
└── css/
└── styles.css # カスタムスタイル定義
クラスベース設計
class ShareholderDialogueApp {
constructor() {
// 状態初期化
this.state = { /* アプリケーション状態 */ };
this.azureConfig = { /* Azure設定 */ };
this.languageConfig = { /* 多言語設定 */ };
}
// 初期化メソッド
async initialize() { /* 起動処理 */ }
// 状態管理メソッド
updateState(key, value) { /* 状態更新 */ }
// API通信メソッド
async callAzureOpenAI(prompt) { /* API呼び出し */ }
// UI更新メソッド
updateUI(element, content) { /* DOM操作 */ }
}
1. 状態管理モジュール
// アプリケーション状態の一元管理
state: {
isConnected: boolean, // Azure接続状態
uploadedFiles: Array, // アップロードファイル一覧
currentPdfData: Object, // 現在のPDFデータ
dialogueHistory: Array, // 対話履歴
conversationTurn: number, // 対話ターン数
pdfContent: string, // 抽出PDFテキスト
isDialogueInProgress: boolean, // 対話進行状態
selectedLanguage: string, // 選択言語
settingsCollapsed: boolean // 設定UI状態
}
2. Azure OpenAI 連携モジュール
async callAzureOpenAI(prompt, maxTokens = 500) {
const response = await fetch(`${this.azureConfig.endpoint}/openai/deployments/${this.azureConfig.deploymentName}/chat/completions?api-version=${this.azureConfig.apiVersion}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': this.azureConfig.apiKey
},
body: JSON.stringify({
messages: [
{ role: 'system', content: prompt },
{ role: 'user', content: this.state.pdfContent }
],
max_tokens: maxTokens,
temperature: 0.7
})
});
return await response.json();
}
3. PDF処理モジュール
async extractTextFromPDF(file) {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
let fullText = '';
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const textContent = await page.getTextContent();
fullText += textContent.items.map(item => item.str).join(' ');
}
return fullText;
}
4. 対話管理モジュール
async generateShareholderQuestion() {
const prompt = this.languageConfig[this.state.selectedLanguage].shareholderPrompt;
const response = await this.callAzureOpenAI(prompt);
return response.choices[0].message.content;
}
async generateDirectorResponse(question) {
const prompt = `${this.languageConfig[this.state.selectedLanguage].directorPrompt}\n\n質問: ${question}`;
const response = await this.callAzureOpenAI(prompt);
return response.choices[0].message.content;
}
設定データの永続化
// Azure OpenAI設定の保存
localStorage.setItem('azureOpenAIConfig', JSON.stringify({
endpoint: string,
apiKey: string,
deploymentName: string,
apiVersion: string
}));
// 言語設定の保存
localStorage.setItem('selectedLanguage', string);
// UI設定の保存
localStorage.setItem('settingsCollapsed', boolean);
Azure OpenAI Chat Completions API
Endpoint: https://{resource}.openai.azure.com/
Path: /openai/deployments/{deployment-name}/chat/completions
Method: POST
Headers:
Content-Type: application/json
api-key: {api-key}
Parameters:
api-version: 2024-02-15-preview
Payload:
messages: Array<{role, content}>
max_tokens: number
temperature: number
graph LR
A[ユーザー入力] --> B[フォームバリデーション]
B --> C[ローカルストレージ暗号化保存]
C --> D[メモリ内での一時使用]
D --> E[HTTPS通信でAPI送信]
F[環境変数] --> C
G[セッション終了] --> H[メモリクリア]
// ローカルストレージでの暗号化保存
const encryptedConfig = btoa(JSON.stringify(config));
localStorage.setItem('azureOpenAIConfig', encryptedConfig);
// 使用時の復号化
const decryptedConfig = JSON.parse(atob(localStorage.getItem('azureOpenAIConfig')));
class ErrorHandler {
static handle(error, context) {
console.error(`🚨 エラー発生 [${context}]:`, error);
// エラータイプ別の処理
if (error.name === 'NetworkError') {
this.showNetworkError();
} else if (error.status === 401) {
this.showAuthError();
} else if (error.status === 429) {
this.showRateLimitError();
} else {
this.showGenericError(error.message);
}
}
static showUserFriendlyMessage(message) {
// センシティブ情報を含まないユーザー向けメッセージ表示
}
}
<!-- CDNの効率的活用 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<!-- 非同期読み込み -->
<script src="js/script.js" defer></script>
class MemoryManager {
static cleanup() {
// 使用済みPDFデータのクリーンアップ
if (this.currentPdfData) {
this.currentPdfData = null;
}
// 不要なイベントリスナーの除去
this.removeEventListeners();
// ガベージコレクションの促進
if (window.gc) {
window.gc();
}
}
}
// トークン数の最適化
const optimizedPrompt = this.truncateContent(originalPrompt, 4000);
// レスポンス時間の制御
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000); // 30秒タイムアウト
fetch(apiUrl, {
signal: controller.signal,
// ... その他のオプション
});
class PerformanceMonitor {
static logApiCall(startTime, endTime, apiType) {
const duration = endTime - startTime;
console.log(`📈 API呼び出し [${apiType}]: ${duration}ms`);
if (duration > 10000) {
console.warn(`⚠️ 長時間API呼び出し検出: ${duration}ms`);
}
}
static logMemoryUsage() {
if (performance.memory) {
const memory = performance.memory;
console.log('💾 メモリ使用量:', {
used: Math.round(memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(memory.totalJSHeapSize / 1024 / 1024) + 'MB',
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
});
}
}
}
// 新しい言語の追加
this.languageConfig.newLanguage = {
name: '新言語名',
shareholderPrompt: 'プロンプトテンプレート',
directorPrompt: 'プロンプトテンプレート',
summaryPrompt: 'プロンプトテンプレート',
startMessage: 'メッセージ',
endMessage: 'メッセージ'
};
// プラグイン方式での機能拡張
class PluginManager {
static registerPlugin(name, plugin) {
this.plugins[name] = plugin;
plugin.initialize();
}
static loadPlugin(name) {
return this.plugins[name];
}
}
// 使用例: 音声読み上げプラグイン
const speechPlugin = {
initialize() {
this.synthesis = window.speechSynthesis;
},
speak(text, language) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = language;
this.synthesis.speak(utterance);
}
};
PluginManager.registerPlugin('speech', speechPlugin);
// 複数のAIプロバイダー対応
class AIProviderFactory {
static createProvider(type, config) {
switch (type) {
case 'azure':
return new AzureOpenAIProvider(config);
case 'openai':
return new OpenAIProvider(config);
case 'claude':
return new ClaudeProvider(config);
default:
throw new Error(`未対応のプロバイダー: ${type}`);
}
}
}
class AzureOpenAIProvider {
async generateResponse(prompt) {
// Azure OpenAI固有の実装
}
}
class Logger {
static levels = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
VERBOSE: 4
};
static log(level, message, data = null) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: Object.keys(this.levels)[level],
message,
data
};
switch (level) {
case this.levels.ERROR:
console.error(`🚨 [${timestamp}] ${message}`, data);
break;
case this.levels.WARN:
console.warn(`⚠️ [${timestamp}] ${message}`, data);
break;
case this.levels.INFO:
console.info(`ℹ️ [${timestamp}] ${message}`, data);
break;
case this.levels.DEBUG:
console.debug(`🐛 [${timestamp}] ${message}`, data);
break;
case this.levels.VERBOSE:
console.log(`📝 [${timestamp}] ${message}`, data);
break;
}
}
}
class MetricsCollector {
static metrics = {
apiCalls: 0,
totalTokensUsed: 0,
averageResponseTime: 0,
errorCount: 0,
userSessions: 0
};
static recordAPICall(tokens, responseTime) {
this.metrics.apiCalls++;
this.metrics.totalTokensUsed += tokens;
this.metrics.averageResponseTime =
(this.metrics.averageResponseTime + responseTime) / 2;
}
static getMetrics() {
return { ...this.metrics };
}
}
# GitHub Pagesでの公開設定
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: './src'
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v1
// CDNフォールバック戦略
const loadTailwindCSS = () => {
const script = document.createElement('script');
script.src = 'https://cdn.tailwindcss.com';
script.onerror = () => {
// フォールバック先の読み込み
const fallback = document.createElement('link');
fallback.rel = 'stylesheet';
fallback.href = './css/tailwind-fallback.css';
document.head.appendChild(fallback);
};
document.head.appendChild(script);
};