序論
バイブコーディング(Vibe Coding)とは、プログラミング作業中に触覚フィードバック(振動や触感)を通じて開発者に情報を伝達する新興技術領域です。この概念は、視覚・聴覚に依存しがちな従来の開発環境に触覚という第三の感覚チャネルを追加することで、コーディング効率の向上、エラー検出の迅速化、そして開発者の身体的負担軽減を目指します。
Google Chrome拡張機能としてバイブコーディングシステムを実装することで、WebベースのIDE(統合開発環境)やオンラインコードエディタにおいて、リアルタイムな触覚フィードバックを提供することが可能になります。本記事では、この革新的なコンセプトの技術的実装方法を、Chrome Extension API v3(Manifest V3)を基盤として詳細に解説します。
第1章:バイブコーディングの技術的基盤と理論背景
1.1 触覚フィードバックの認知科学的根拠
触覚フィードバックがプログラミング作業に与える影響については、認知負荷理論(Cognitive Load Theory)の観点から説明できます。人間の認知処理システムは、視覚・聴覚・触覚の各チャネルで並列処理が可能であり、適切に設計された触覚フィードバックは、視覚チャネルの過負荷を軽減しながら重要な情報を伝達できます。
// 認知負荷分散の概念的モデル
class CognitiveLoadManager {
constructor() {
this.visualLoad = 0; // 視覚的認知負荷
this.auditoryLoad = 0; // 聴覚的認知負荷
this.hapticLoad = 0; // 触覚的認知負荷
this.maxLoad = 100; // 各チャネルの最大処理能力
}
optimizeLoadDistribution(visualTask, auditoryTask, hapticTask) {
// 負荷分散アルゴリズム
const totalLoad = visualTask + auditoryTask + hapticTask;
if (this.visualLoad + visualTask > this.maxLoad) {
// 視覚負荷が過多の場合、触覚チャネルにタスクを移行
return this.redistributeToHaptic(visualTask);
}
return { visual: visualTask, auditory: auditoryTask, haptic: hapticTask };
}
}
1.2 Web Vibration APIの技術仕様
現代のWebブラウザにおける触覚フィードバックの実装は、W3C標準であるVibration APIに依存します。このAPIは、navigator.vibrate()
メソッドを通じてデバイスの振動機能にアクセスを提供します。
// Vibration APIの基本実装例
class VibrationController {
constructor() {
this.isSupported = 'vibrate' in navigator;
this.patterns = {
error: [200, 100, 200], // エラー: 長-短-長
warning: [100, 50, 100], // 警告: 短-極短-短
success: [50], // 成功: 単発短
breakpoint: [300, 200, 100] // ブレークポイント: 複雑パターン
};
}
triggerFeedback(type, customPattern = null) {
if (!this.isSupported) {
console.warn('Vibration API not supported on this device');
return false;
}
const pattern = customPattern || this.patterns[type];
if (!pattern) {
throw new Error(`Unknown vibration pattern: ${type}`);
}
return navigator.vibrate(pattern);
}
}
1.3 Chrome Extension Architecture for Haptic Integration
Chrome拡張機能におけるバイブコーディング実装は、Content Script、Background Service Worker、そしてWebページとの相互作用という三層アーキテクチャで構成されます。
コンポーネント | 役割 | API制限 | セキュリティレベル |
---|---|---|---|
Content Script | DOM操作・イベント監視 | Web API制限あり | サンドボックス化 |
Background Service Worker | 永続化・設定管理 | Chrome API全権限 | 特権レベル |
Web Page Context | 直接的な触覚実行 | Web API制限あり | ユーザー許可依存 |
第2章:Chrome拡張機能の基盤実装
2.1 Manifest V3による拡張機能設定
Manifest V3は、セキュリティ強化とパフォーマンス向上を目的としたChrome拡張機能の最新仕様です。バイブコーディング拡張機能では、特にPermissionsの適切な設定が重要になります。
{
"manifest_version": 3,
"name": "VibeCoding - Haptic Feedback for Developers",
"version": "1.0.0",
"description": "Provides tactile feedback for coding activities through vibration patterns",
"permissions": [
"activeTab",
"storage",
"scripting"
],
"host_permissions": [
"https://github.com/*",
"https://codepen.io/*",
"https://jsfiddle.net/*",
"https://codesandbox.io/*",
"https://repl.it/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"css": ["styles.css"]
}
],
"action": {
"default_popup": "popup.html",
"default_title": "VibeCoding Settings"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
2.2 Background Service Workerの実装
Service Workerは拡張機能のバックグラウンド処理を担当し、設定の永続化、メッセージハンドリング、そして複数タブ間での状態同期を管理します。
// background.js
class VibeCodingBackgroundService {
constructor() {
this.settings = {
enabled: true,
intensity: 'medium',
patterns: {
syntaxError: [150, 50, 150, 50, 150],
autoComplete: [25],
breakpointHit: [200, 100, 100, 100, 200],
testPass: [50, 25, 50],
testFail: [200, 100, 200, 100, 200, 100, 200]
},
enabledSites: ['github.com', 'codepen.io', 'jsfiddle.net']
};
this.initializeEventListeners();
this.loadSettings();
}
initializeEventListeners() {
// 拡張機能インストール時の初期化
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
this.initializeDefaultSettings();
}
});
// Content Scriptからのメッセージハンドリング
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
this.handleMessage(message, sender, sendResponse);
return true; // 非同期レスポンスを有効化
});
// タブの変更監視
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && this.isEnabledSite(tab.url)) {
this.injectVibrationHandler(tabId);
}
});
}
async handleMessage(message, sender, sendResponse) {
switch (message.type) {
case 'GET_SETTINGS':
const settings = await this.getSettings();
sendResponse({ success: true, data: settings });
break;
case 'UPDATE_SETTINGS':
await this.updateSettings(message.data);
sendResponse({ success: true });
break;
case 'TRIGGER_VIBRATION':
await this.broadcastVibration(message.pattern, message.intensity);
sendResponse({ success: true });
break;
default:
sendResponse({ success: false, error: 'Unknown message type' });
}
}
async loadSettings() {
try {
const result = await chrome.storage.sync.get('vibeCodingSettings');
if (result.vibeCodingSettings) {
this.settings = { ...this.settings, ...result.vibeCodingSettings };
}
} catch (error) {
console.error('Failed to load settings:', error);
}
}
async updateSettings(newSettings) {
this.settings = { ...this.settings, ...newSettings };
await chrome.storage.sync.set({ vibeCodingSettings: this.settings });
// 全アクティブタブに設定変更を通知
const tabs = await chrome.tabs.query({});
tabs.forEach(tab => {
if (this.isEnabledSite(tab.url)) {
chrome.tabs.sendMessage(tab.id, {
type: 'SETTINGS_UPDATED',
data: this.settings
}).catch(() => {
// タブが応答しない場合は無視(非アクティブタブなど)
});
}
});
}
isEnabledSite(url) {
if (!url) return false;
return this.settings.enabledSites.some(site => url.includes(site));
}
async injectVibrationHandler(tabId) {
try {
await chrome.scripting.executeScript({
target: { tabId },
function: this.initializeVibrationSystem,
args: [this.settings]
});
} catch (error) {
console.error('Failed to inject vibration handler:', error);
}
}
// タブに注入される初期化関数
initializeVibrationSystem(settings) {
if (!window.vibeCodingInitialized) {
window.vibeCodingInitialized = true;
window.vibeCodingSettings = settings;
// グローバル振動制御関数を定義
window.triggerVibeCoding = function(patternName) {
const pattern = settings.patterns[patternName];
if (pattern && navigator.vibrate) {
navigator.vibrate(pattern);
}
};
}
}
}
// Service Workerの初期化
new VibeCodingBackgroundService();
2.3 Content Scriptによるページ解析とイベント検出
Content Scriptは、Webページ上でのコーディング活動を監視し、適切なタイミングで触覚フィードバックをトリガーする役割を担います。
// content-script.js
class VibeCodingContentAnalyzer {
constructor() {
this.initialized = false;
this.settings = null;
this.codeEditors = new Map(); // 検出されたエディタインスタンス
this.lastErrorCount = 0;
this.typingTimer = null;
this.keySequence = [];
this.initialize();
}
async initialize() {
await this.loadSettings();
await this.loadStatistics();
this.setupEventListeners();
this.renderPatternGrid();
this.updateUI();
this.applyAccessibilitySettings();
}
async loadSettings() {
try {
const response = await chrome.runtime.sendMessage({ type: 'GET_SETTINGS' });
if (response.success) {
this.settings = response.data;
} else {
this.settings = this.getDefaultSettings();
}
} catch (error) {
console.error('Failed to load settings:', error);
this.settings = this.getDefaultSettings();
}
}
async loadStatistics() {
try {
const response = await chrome.runtime.sendMessage({ type: 'GET_STATISTICS' });
if (response.success) {
this.statistics = response.data;
} else {
this.statistics = this.getDefaultStatistics();
}
} catch (error) {
console.error('Failed to load statistics:', error);
this.statistics = this.getDefaultStatistics();
}
}
getDefaultSettings() {
return {
enabled: true,
intensity: 50,
patterns: {
syntaxError: [200, 100, 200],
autoComplete: [50],
breakpointHit: [300, 200, 100],
testPass: [100, 50, 100],
testFail: [150, 50, 150, 50, 150],
debugStep: [80, 40, 80]
},
accessibility: {
reduceMotion: false,
highContrast: false,
largeFonts: false,
screenReaderMode: false
}
};
}
getDefaultStatistics() {
return {
todayTriggers: 0,
weeklyTriggers: [],
patternUsage: new Map(),
effectivenessRatings: new Map(),
lastUpdated: Date.now()
};
}
setupEventListeners() {
// メイン有効/無効切り替え
const enabledToggle = document.getElementById('enabledToggle');
enabledToggle.checked = this.settings.enabled;
enabledToggle.addEventListener('change', this.handleEnabledToggle.bind(this));
// 強度スライダー
const intensitySlider = document.getElementById('intensitySlider');
intensitySlider.value = this.settings.intensity;
intensitySlider.addEventListener('input', this.handleIntensityChange.bind(this));
// アクセシビリティ設定
this.setupAccessibilityListeners();
// キーボードナビゲーション
this.setupKeyboardNavigation();
// アニメーション完了イベント
document.addEventListener('animationend', this.handleAnimationEnd.bind(this));
}
setupAccessibilityListeners() {
const accessibilityControls = {
'reduceMotion': 'reduceMotion',
'highContrast': 'highContrast',
'largeFonts': 'largeFonts',
'screenReaderMode': 'screenReaderMode'
};
Object.entries(accessibilityControls).forEach([id, setting] => {
const element = document.getElementById(id);
if (element) {
element.checked = this.settings.accessibility[setting];
element.addEventListener('change', (e) => {
this.handleAccessibilityChange(setting, e.target.checked);
});
}
});
}
setupKeyboardNavigation() {
// Tab navigation support
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
this.highlightFocusedElement(e);
} else if (e.key === 'Enter' || e.key === ' ') {
this.activateFocusedElement(e);
} else if (e.key === 'Escape') {
window.close();
}
});
// ARIA live region for screen readers
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.style.position = 'absolute';
liveRegion.style.left = '-9999px';
liveRegion.id = 'liveRegion';
document.body.appendChild(liveRegion);
}
renderPatternGrid() {
const patternGrid = document.getElementById('patternGrid');
patternGrid.innerHTML = '';
this.patternTypes.forEach(patternType => {
const patternItem = this.createPatternItem(patternType);
patternGrid.appendChild(patternItem);
});
}
createPatternItem(patternType) {
const item = document.createElement('div');
item.className = 'pattern-item';
item.setAttribute('data-pattern', patternType.id);
item.setAttribute('tabindex', '0');
item.setAttribute('role', 'button');
item.setAttribute('aria-label', `${patternType.name}パターンをテスト`);
item.innerHTML = `
<div style="font-size: 16px; margin-bottom: 4px;">${patternType.icon}</div>
<div style="font-size: 11px; line-height: 1.2;">${patternType.name}</div>
<button class="test-button" data-pattern="${patternType.id}"
aria-label="${patternType.name}をテスト" style="margin-top: 4px;">
テスト
</button>
`;
// クリックイベント
item.addEventListener('click', (e) => {
if (e.target.classList.contains('test-button')) {
e.stopPropagation();
this.testPattern(patternType.id);
} else {
this.selectPattern(patternType.id);
}
});
// キーボードサポート
item.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (e.target.classList.contains('test-button')) {
this.testPattern(patternType.id);
} else {
this.selectPattern(patternType.id);
}
}
});
return item;
}
async handleEnabledToggle(e) {
this.settings.enabled = e.target.checked;
await this.saveSettings();
this.updateAccessibilityAnnouncement(
`バイブコーディングが${e.target.checked ? '有効' : '無効'}になりました`
);
}
async handleIntensityChange(e) {
this.settings.intensity = parseInt(e.target.value);
await this.saveSettings();
// リアルタイムフィードバック
if (navigator.vibrate) {
const testPattern = [this.settings.intensity * 2];
navigator.vibrate(testPattern);
}
this.updateAccessibilityAnnouncement(`振動強度が${this.settings.intensity}%に設定されました`);
}
async handleAccessibilityChange(setting, value) {
this.settings.accessibility[setting] = value;
await this.saveSettings();
this.applyAccessibilitySettings();
const settingNames = {
reduceMotion: 'アニメーション削減',
highContrast: '高コントラストモード',
largeFonts: '大きなフォント',
screenReaderMode: 'スクリーンリーダー対応'
};
this.updateAccessibilityAnnouncement(
`${settingNames[setting]}が${value ? '有効' : '無効'}になりました`
);
}
applyAccessibilitySettings() {
const body = document.body;
// アニメーション削減
if (this.settings.accessibility.reduceMotion) {
body.classList.add('reduce-motion');
} else {
body.classList.remove('reduce-motion');
}
// 高コントラストモード
if (this.settings.accessibility.highContrast) {
body.classList.add('high-contrast');
} else {
body.classList.remove('high-contrast');
}
// 大きなフォント
if (this.settings.accessibility.largeFonts) {
body.classList.add('large-fonts');
} else {
body.classList.remove('large-fonts');
}
// スクリーンリーダー対応
if (this.settings.accessibility.screenReaderMode) {
body.classList.add('screen-reader-mode');
this.enhanceScreenReaderSupport();
} else {
body.classList.remove('screen-reader-mode');
}
}
enhanceScreenReaderSupport() {
// 追加のARIA属性とランドマーク
const sections = document.querySelectorAll('.section');
sections.forEach((section, index) => {
section.setAttribute('role', 'region');
const heading = section.querySelector('h2');
if (heading) {
const headingId = `section-heading-${index}`;
heading.id = headingId;
section.setAttribute('aria-labelledby', headingId);
}
});
// 統計データの説明を追加
const statItems = document.querySelectorAll('.stat-item');
statItems.forEach(item => {
const value = item.querySelector('.stat-value');
const label = item.querySelector('.stat-label');
if (value && label) {
value.setAttribute('aria-describedby', label.id || '');
}
});
}
async testPattern(patternId) {
const patternType = this.patternTypes.find(p => p.id === patternId);
if (!patternType) return;
// ビジュアルフィードバック
const patternItem = document.querySelector(`[data-pattern="${patternId}"]`);
patternItem.classList.add('testing');
try {
// 振動実行
if (navigator.vibrate) {
const adjustedPattern = patternType.testPattern.map(duration =>
Math.floor(duration * (this.settings.intensity / 100))
);
navigator.vibrate(adjustedPattern);
}
// Background serviceにもテスト要求を送信
await chrome.runtime.sendMessage({
type: 'TEST_VIBRATION',
pattern: patternId
});
this.updateAccessibilityAnnouncement(`${patternType.name}パターンをテストしました`);
// 使用統計の更新
this.updatePatternUsageStats(patternId);
} catch (error) {
console.error('Pattern test failed:', error);
this.updateAccessibilityAnnouncement('パターンテストに失敗しました');
} finally {
// ビジュアルフィードバックの解除
setTimeout(() => {
patternItem.classList.remove('testing');
}, 1000);
}
}
selectPattern(patternId) {
// パターン選択の視覚的フィードバック
document.querySelectorAll('.pattern-item').forEach(item => {
item.classList.remove('active');
});
const selectedItem = document.querySelector(`[data-pattern="${patternId}"]`);
selectedItem.classList.add('active');
// 選択されたパターンの詳細表示(将来の機能拡張用)
this.showPatternDetails(patternId);
}
showPatternDetails(patternId) {
const patternType = this.patternTypes.find(p => p.id === patternId);
if (!patternType) return;
// 詳細情報の表示(モーダルまたはサイドパネル)
console.log(`Selected pattern: ${patternType.name}`);
// アクセシビリティアナウンス
this.updateAccessibilityAnnouncement(`${patternType.name}パターンが選択されました`);
}
updateUI() {
this.updateStatistics();
this.updatePatternStates();
}
updateStatistics() {
// 今日のトリガー数
const todayTriggersElement = document.getElementById('todayTriggers');
todayTriggersElement.textContent = this.statistics.todayTriggers.toString();
// 週平均の計算
const weeklyAverage = this.calculateWeeklyAverage();
const weeklyAverageElement = document.getElementById('weeklyAverage');
weeklyAverageElement.textContent = Math.round(weeklyAverage).toString();
// 最頻使用パターン
const mostUsedPattern = this.getMostUsedPattern();
const mostUsedPatternElement = document.getElementById('mostUsedPattern');
mostUsedPatternElement.textContent = mostUsedPattern || '-';
// 効果性スコア
const effectivenessScore = this.calculateEffectivenessScore();
const effectivenessScoreElement = document.getElementById('effectivenessScore');
effectivenessScoreElement.textContent = `${Math.round(effectivenessScore * 100)}%`;
}
calculateWeeklyAverage() {
if (!this.statistics.weeklyTriggers || this.statistics.weeklyTriggers.length === 0) {
return 0;
}
const sum = this.statistics.weeklyTriggers.reduce((acc, count) => acc + count, 0);
return sum / this.statistics.weeklyTriggers.length;
}
getMostUsedPattern() {
if (!this.statistics.patternUsage || this.statistics.patternUsage.size === 0) {
return null;
}
let maxUsage = 0;
let mostUsedPattern = null;
for (const [pattern, usage] of this.statistics.patternUsage) {
if (usage > maxUsage) {
maxUsage = usage;
mostUsedPattern = pattern;
}
}
// パターンIDを日本語名に変換
const patternType = this.patternTypes.find(p => p.id === mostUsedPattern);
return patternType ? patternType.name : mostUsedPattern;
}
calculateEffectivenessScore() {
if (!this.statistics.effectivenessRatings || this.statistics.effectivenessRatings.size === 0) {
return 0.5; // デフォルト値
}
let totalRating = 0;
let ratingCount = 0;
for (const [pattern, rating] of this.statistics.effectivenessRatings) {
totalRating += rating;
ratingCount++;
}
return ratingCount > 0 ? totalRating / ratingCount : 0.5;
}
updatePatternStates() {
// パターンアイテムの状態を更新
this.patternTypes.forEach(patternType => {
const patternItem = document.querySelector(`[data-pattern="${patternType.id}"]`);
const usage = this.statistics.patternUsage.get(patternType.id) || 0;
const effectiveness = this.statistics.effectivenessRatings.get(patternType.id) || 0.5;
// 使用頻度に応じた視覚的インジケーター
if (usage > 10) {
patternItem.classList.add('frequently-used');
} else {
patternItem.classList.remove('frequently-used');
}
// 効果性に応じた視覚的インジケーター
if (effectiveness > 0.7) {
patternItem.classList.add('highly-effective');
} else if (effectiveness < 0.3) {
patternItem.classList.add('low-effective');
}
// ツールチップ情報の更新
patternItem.title = `使用回数: ${usage}, 効果性: ${Math.round(effectiveness * 100)}%`;
});
}
updatePatternUsageStats(patternId) {
// 統計の更新
this.statistics.todayTriggers++;
const currentUsage = this.statistics.patternUsage.get(patternId) || 0;
this.statistics.patternUsage.set(patternId, currentUsage + 1);
// UI の即座更新
this.updateStatistics();
// Background serviceに統計更新を通知
chrome.runtime.sendMessage({
type: 'UPDATE_STATISTICS',
data: {
patternId,
timestamp: Date.now()
}
});
}
updateAccessibilityAnnouncement(message) {
const liveRegion = document.getElementById('liveRegion');
if (liveRegion) {
liveRegion.textContent = message;
}
}
highlightFocusedElement(e) {
// フォーカス可能な要素のハイライト強化
const focusedElement = document.activeElement;
if (focusedElement && focusedElement !== document.body) {
focusedElement.classList.add('keyboard-focused');
// 前のフォーカス要素のハイライト解除
document.querySelectorAll('.keyboard-focused').forEach(el => {
if (el !== focusedElement) {
el.classList.remove('keyboard-focused');
}
});
}
}
activateFocusedElement(e) {
const focusedElement = document.activeElement;
if (focusedElement.classList.contains('toggle-switch') ||
focusedElement.classList.contains('pattern-item') ||
focusedElement.classList.contains('test-button')) {
e.preventDefault();
focusedElement.click();
}
}
handleAnimationEnd(e) {
// アニメーション完了後のクリーンアップ
if (e.target.classList.contains('pattern-item')) {
e.target.classList.remove('testing');
}
}
async saveSettings() {
try {
await chrome.runtime.sendMessage({
type: 'UPDATE_SETTINGS',
data: this.settings
});
} catch (error) {
console.error('Failed to save settings:', error);
this.updateAccessibilityAnnouncement('設定の保存に失敗しました');
}
}
}
// ポップアップの初期化
document.addEventListener('DOMContentLoaded', () => {
new VibeCodingPopupController();
});
// 追加のCSS(アクセシビリティ強化)
const additionalStyles = `
.reduce-motion * {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.high-contrast {
filter: contrast(150%) brightness(120%);
}
.high-contrast .section {
border: 2px solid white;
}
.large-fonts {
font-size: 18px !important;
}
.large-fonts .section h2 {
font-size: 20px !important;
}
.large-fonts .pattern-item {
font-size: 16px !important;
}
.screen-reader-mode .pattern-item {
outline: 2px solid transparent;
}
.screen-reader-mode .pattern-item:focus {
outline: 2px solid #4CAF50;
outline-offset: 2px;
}
.keyboard-focused {
outline: 3px solid #4CAF50 !important;
outline-offset: 2px !important;
}
.pattern-item.testing {
animation: pulse 0.5s ease-in-out;
background: rgba(76, 175, 80, 0.4) !important;
}
.pattern-item.frequently-used::after {
content: "🔥";
position: absolute;
top: 2px;
right: 2px;
font-size: 12px;
}
.pattern-item.highly-effective::after {
content: "⭐";
position: absolute;
top: 2px;
left: 2px;
font-size: 12px;
}
.pattern-item.low-effective {
opacity: 0.6;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@media (prefers-color-scheme: dark) {
.high-contrast {
filter: contrast(200%) brightness(80%);
}
}
@media (max-width: 400px) {
body {
width: 100vw;
padding: 12px;
}
.pattern-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
`;
// スタイル注入
const styleSheet = document.createElement('style');
styleSheet.textContent = additionalStyles;
document.head.appendChild(styleSheet);
第4章:実装時の限界とリスクの分析
4.1 技術的制約と対応策
バイブコーディングシステムの実装には、Web技術とデバイス制約による複数の技術的限界が存在します。これらの制約を理解し、適切な対応策を講じることが重要です。
制約要因 | 具体的な問題 | 対応策 | 実装難易度 |
---|---|---|---|
Vibration API制限 | iOS Safariでの未サポート | 代替フィードバック手法の実装 | 高 |
バッテリー消費 | 過度な振動によるバッテリー劣化 | 適応的強度制御システム | 中 |
ユーザー許可 | 自動振動の許可取得 | 段階的許可プロセス | 低 |
精度の限界 | 振動パターンの知覚精度差 | 個人適応アルゴリズム | 高 |
4.2 プライバシーとセキュリティの考慮事項
バイブコーディングシステムは、開発者のコーディング行動パターンという機密性の高いデータを扱うため、プライバシー保護とセキュリティ対策が重要です。
// プライバシー保護機能の実装
class PrivacyProtectionManager {
constructor() {
this.encryptionKey = null;
this.dataRetentionPeriod = 30 * 24 * 60 * 60 * 1000; // 30日
this.sensitiveDataMask = new Map();
this.initializeEncryption();
}
async initializeEncryption() {
// Web Crypto APIを使用した暗号化キーの生成
this.encryptionKey = await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
false, // キーの抽出を禁止
['encrypt', 'decrypt']
);
}
async encryptSensitiveData(data) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
this.encryptionKey,
dataBuffer
);
return {
encryptedData: Array.from(new Uint8Array(encryptedData)),
iv: Array.from(iv),
timestamp: Date.now()
};
}
async decryptSensitiveData(encryptedPackage) {
const encryptedData = new Uint8Array(encryptedPackage.encryptedData);
const iv = new Uint8Array(encryptedPackage.iv);
const decryptedBuffer = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
this.encryptionKey,
encryptedData
);
const decoder = new TextDecoder();
const decryptedString = decoder.decode(decryptedBuffer);
return JSON.parse(decryptedString);
}
sanitizeCodeContent(codeContent) {
// コード内容から機密情報を除去
const sensitivePatterns = [
/(['"])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\1/g, // メールアドレス
/(['"])(https?:\/\/[^\s'"]+)\1/g, // URL
/(['"])[A-Za-z0-9+/]{20,}={0,2}\1/g, // Base64文字列(APIキー可能性)
/password\s*[:=]\s*['"][^'"]+['"]/gi, // パスワード
/api[_-]?key\s*[:=]\s*['"][^'"]+['"]/gi, // APIキー
/token\s*[:=]\s*['"][^'"]+['"]/gi, // トークン
];
let sanitizedContent = codeContent;
sensitivePatterns.forEach((pattern, index) => {
sanitizedContent = sanitizedContent.replace(pattern, (match) => {
const placeholder = `[REDACTED_${index}]`;
this.sensitiveDataMask.set(placeholder, match);
return placeholder;
});
});
return sanitizedContent;
}
cleanupExpiredData() {
const now = Date.now();
const expiredData = [];
// 期限切れデータの識別
this.sensitiveDataMask.forEach((value, key) => {
if (now - value.timestamp > this.dataRetentionPeriod) {
expiredData.push(key);
}
});
// 期限切れデータの削除
expiredData.forEach(key => {
this.sensitiveDataMask.delete(key);
});
return expiredData.length;
}
generatePrivacyReport() {
return {
dataRetentionPeriod: this.dataRetentionPeriod,
encryptedDataCount: this.sensitiveDataMask.size,
lastCleanup: this.getLastCleanupTime(),
securityLevel: this.assessSecurityLevel()
};
}
assessSecurityLevel() {
const factors = {
encryptionEnabled: !!this.encryptionKey,
dataSanitizationActive: this.sensitiveDataMask.size >= 0,
regularCleanup: this.getLastCleanupTime() < 24 * 60 * 60 * 1000
};
const securityScore = Object.values(factors).filter(Boolean).length / Object.values(factors).length;
if (securityScore >= 0.8) return 'HIGH';
if (securityScore >= 0.6) return 'MEDIUM';
return 'LOW';
}
getLastCleanupTime() {
// 最後のクリーンアップ時刻を取得(実装は省略)
return Date.now() - (Math.random() * 24 * 60 * 60 * 1000);
}
}
4.3 パフォーマンス最適化とリソース管理
リアルタイムなコーディング活動の監視は、ブラウザのパフォーマンスに大きな影響を与える可能性があります。効率的なリソース管理が必要です。
// パフォーマンス最適化マネージャー
class PerformanceOptimizationManager {
constructor() {
this.eventQueue = [];
this.processingTimer = null;
this.performanceMetrics = {
averageProcessingTime: 0,
memoryUsage: 0,
eventProcessingRate: 0,
lastOptimization: Date.now()
};
this.optimizationStrategies = {
debouncing: true,
eventBatching: true,
memoryPooling: true,
lazyLoading: true
};
this.initializePerformanceMonitoring();
}
initializePerformanceMonitoring() {
// Performance Observer APIによる監視
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
this.analyzePerformanceEntries(list.getEntries());
});
observer.observe({ entryTypes: ['measure', 'navigation'] });
}
// メモリ使用量の定期監視
setInterval(() => {
this.monitorMemoryUsage();
}, 30000); // 30秒間隔
}
queueEvent(event) {
this.eventQueue.push({
...event,
timestamp: performance.now(),
id: this.generateEventId()
});
// イベントキューのサイズ制限
if (this.eventQueue.length > 1000) {
this.eventQueue.shift();
}
this.scheduleProcessing();
}
scheduleProcessing() {
if (this.processingTimer) return;
// デバウンシング: 50ms間隔でイベント処理
this.processingTimer = setTimeout(() => {
this.processBatchedEvents();
this.processingTimer = null;
}, 50);
}
processBatchedEvents() {
const startTime = performance.now();
const batchSize = Math.min(this.eventQueue.length, 20);
const batch = this.eventQueue.splice(0, batchSize);
// イベントの種類別グループ化
const groupedEvents = this.groupEventsByType(batch);
// 効率的な並列処理
const processingPromises = Object.entries(groupedEvents).map(([type, events]) => {
return this.processEventGroup(type, events);
});
Promise.all(processingPromises).then(() => {
const endTime = performance.now();
this.updatePerformanceMetrics(endTime - startTime, batchSize);
});
}
groupEventsByType(events) {
return events.reduce((groups, event) => {
const type = event.type || 'default';
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(event);
return groups;
}, {});
}
async processEventGroup(type, events) {
switch (type) {
case 'keydown':
return this.processKeyboardEvents(events);
case 'content_change':
return this.processContentChangeEvents(events);
case 'error_detection':
return this.processErrorEvents(events);
default:
return this.processGenericEvents(events);
}
}
processKeyboardEvents(events) {
// キーボードイベントの最適化処理
const keySequence = events.map(e => e.key).join('');
const patterns = this.detectKeyPatterns(keySequence);
return patterns.map(pattern => ({
type: 'pattern_detected',
pattern: pattern,
confidence: this.calculatePatternConfidence(pattern)
}));
}
processContentChangeEvents(events) {
// コンテンツ変更イベントの差分処理
if (events.length === 0) return [];
const firstEvent = events[0];
const lastEvent = events[events.length - 1];
const contentDiff = this.calculateContentDiff(
firstEvent.previousContent,
lastEvent.currentContent
);
return [{
type: 'content_diff',
diff: contentDiff,
changeCount: events.length,
timeSpan: lastEvent.timestamp - firstEvent.timestamp
}];
}
processErrorEvents(events) {
// エラーイベントの集約処理
const errorsByType = events.reduce((errors, event) => {
const errorType = event.errorType || 'unknown';
if (!errors[errorType]) {
errors[errorType] = [];
}
errors[errorType].push(event);
return errors;
}, {});
return Object.entries(errorsByType).map(([type, typeEvents]) => ({
type: 'error_summary',
errorType: type,
count: typeEvents.length,
severity: this.calculateErrorSeverity(typeEvents)
}));
}
processGenericEvents(events) {
// 汎用イベント処理
return events.map(event => ({
...event,
processed: true,
processingTime: performance.now()
}));
}
detectKeyPatterns(keySequence) {
const patterns = [];
// 繰り返しパターンの検出
const repetitionPattern = /(.{1,10})\1{2,}/g;
let match;
while ((match = repetitionPattern.exec(keySequence)) !== null) {
patterns.push({
type: 'repetition',
sequence: match[1],
count: Math.floor(match[0].length / match[1].length)
});
}
// 交互パターンの検出
const alternatingPattern = /(.)(.)(\1\2){2,}/g;
while ((match = alternatingPattern.exec(keySequence)) !== null) {
patterns.push({
type: 'alternating',
keys: [match[1], match[2]],
count: Math.floor(match[0].length / 2)
});
}
return patterns;
}
calculatePatternConfidence(pattern) {
const baseConfidence = 0.5;
const lengthBonus = Math.min(pattern.count * 0.1, 0.4);
return Math.min(baseConfidence + lengthBonus, 1.0);
}
calculateContentDiff(previousContent, currentContent) {
if (!previousContent || !currentContent) {
return { type: 'full_change', added: currentContent?.length || 0, removed: 0 };
}
// 簡単な差分計算(実際の実装では高度なアルゴリズムを使用)
const addedChars = Math.max(0, currentContent.length - previousContent.length);
const removedChars = Math.max(0, previousContent.length - currentContent.length);
return {
type: 'incremental_change',
added: addedChars,
removed: removedChars,
netChange: currentContent.length - previousContent.length
};
}
calculateErrorSeverity(errorEvents) {
const severityWeights = {
syntax: 0.8,
runtime: 1.0,
logical: 0.6,
warning: 0.3
};
const totalWeight = errorEvents.reduce((sum, event) => {
const weight = severityWeights[event.errorSubtype] || 0.5;
return sum + weight;
}, 0);
return Math.min(totalWeight / errorEvents.length, 1.0);
}
updatePerformanceMetrics(processingTime, eventCount) {
// 移動平均による処理時間の更新
const alpha = 0.1; // 平滑化係数
this.performanceMetrics.averageProcessingTime =
(1 - alpha) * this.performanceMetrics.averageProcessingTime + alpha * processingTime;
// イベント処理レートの更新
this.performanceMetrics.eventProcessingRate = eventCount / (processingTime / 1000);
// 最適化の判断
if (this.shouldOptimize()) {
this.performOptimization();
}
}
shouldOptimize() {
const thresholds = {
averageProcessingTime: 100, // 100ms
memoryUsage: 50 * 1024 * 1024, // 50MB
eventProcessingRate: 10 // 10 events/sec minimum
};
return this.performanceMetrics.averageProcessingTime > thresholds.averageProcessingTime ||
this.performanceMetrics.memoryUsage > thresholds.memoryUsage ||
this.performanceMetrics.eventProcessingRate < thresholds.eventProcessingRate;
}
performOptimization() {
console.log('Performing performance optimization...');
// イベントキューのクリーンアップ
this.cleanupEventQueue();
// メモリの最適化
this.optimizeMemoryUsage();
// 処理戦略の調整
this.adjustProcessingStrategy();
this.performanceMetrics.lastOptimization = Date.now();
}
cleanupEventQueue() {
// 古いイベントの削除
const cutoffTime = performance.now() - 300000; // 5分前
this.eventQueue = this.eventQueue.filter(event => event.timestamp > cutoffTime);
}
optimizeMemoryUsage() {
// ガベージコレクションの強制実行(可能な場合)
if (window.gc) {
window.gc();
}
// 不要なオブジェクト参照の削除
this.clearUnusedReferences();
}
clearUnusedReferences() {
// WeakMapとWeakSetを使用したオブジェクト参照の管理
if (this.cachedData) {
this.cachedData.clear();
}
}
adjustProcessingStrategy() {
// パフォーマンス状況に応じた処理戦略の動的調整
if (this.performanceMetrics.averageProcessingTime > 50) {
// 処理時間が長い場合は、バッチサイズを削減
this.optimizationStrategies.batchSize = Math.max(
5,
Math.floor(this.optimizationStrategies.batchSize * 0.8)
);
} else if (this.performanceMetrics.averageProcessingTime < 20) {
// 処理時間が短い場合は、バッチサイズを増加
this.optimizationStrategies.batchSize = Math.min(
50,
Math.floor(this.optimizationStrategies.batchSize * 1.2)
);
}
}
monitorMemoryUsage() {
if (performance.memory) {
this.performanceMetrics.memoryUsage = performance.memory.usedJSHeapSize;
// メモリ使用量のログ出力(デバッグ用)
console.debug('Memory usage:', {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + 'MB',
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
});
}
}
analyzePerformanceEntries(entries) {
entries.forEach(entry => {
if (entry.entryType === 'measure') {
// カスタムパフォーマンス測定の分析
this.processMeasureEntry(entry);
}
});
}
processMeasureEntry(entry) {
console.debug(`Performance measure: ${entry.name} took ${entry.duration}ms`);
// パフォーマンス測定結果に基づく最適化判断
if (entry.duration > 100 && entry.name.includes('vibeCoding')) {
console.warn(`Slow performance detected in ${entry.name}`);
this.scheduleOptimization();
}
}
scheduleOptimization() {
// 最適化の遅延実行
setTimeout(() => {
this.performOptimization();
}, 1000);
}
generateEventId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
getPerformanceReport() {
return {
...this.performanceMetrics,
eventQueueSize: this.eventQueue.length,
optimizationStrategies: this.optimizationStrategies,
lastUpdate: Date.now()
};
}
}
第5章:不適切なユースケースと倫理的考慮
5.1 技術的制約による不適切な利用
バイブコーディング技術には明確な適用限界があり、以下の用途での使用は推奨されません。
医療・生命維持システムでの使用禁止 触覚フィードバックの信頼性は100%ではなく、重要な警告やエラー通知を振動のみに依存することは危険です。医療機器制御、航空管制システム、原子力発電所制御システムなどの生命に関わるソフトウェア開発では、主要な通知手段として使用すべきではありません。
プライバシー侵害の可能性 コーディング行動パターンの詳細な記録は、開発者の個人的な作業スタイル、技術レベル、さらには健康状態まで推測可能な情報を含む可能性があります。企業による従業員監視や、技術的能力の不当な評価に使用されるリスクがあります。
アクセシビリティの誤用 触覚フィードバックは視覚・聴覚障がい者のアクセシビリティ向上に有効ですが、唯一の通知手段として依存することは適切ではありません。触覚認知能力にも個人差があり、包括的なアクセシビリティ設計の一部として位置づけるべきです。
5.2 倫理的ガイドラインと責任ある開発
// 倫理的利用チェッカー
class EthicalUsageChecker {
constructor() {
this.ethicalGuidelines = {
privacyProtection: true,
consentManagement: true,
dataMinimization: true,
transparentOperation: true,
accessibilityFirst: true
};
this.prohibitedUses = [
'employee_surveillance',
'skill_assessment_without_consent',
'health_monitoring_unauthorized',
'productivity_enforcement',
'discriminatory_profiling'
];
}
validateUsageIntent(usageDescription) {
const ethicalScore = this.calculateEthicalScore(usageDescription);
const prohibitedElements = this.detectProhibitedElements(usageDescription);
return {
isEthical: ethicalScore > 0.7 && prohibitedElements.length === 0,
score: ethicalScore,
prohibitedElements,
recommendations: this.generateEthicalRecommendations(ethicalScore, prohibitedElements)
};
}
calculateEthicalScore(description) {
const positiveIndicators = [
'user consent', 'privacy protection', 'accessibility improvement',
'voluntary use', 'transparent operation', 'user control'
];
const negativeIndicators = [
'monitoring', 'surveillance', 'mandatory', 'hidden operation',
'data collection without consent', 'performance evaluation'
];
let score = 0.5; // ベーススコア
positiveIndicators.forEach(indicator => {
if (description.toLowerCase().includes(indicator)) {
score += 0.1;
}
});
negativeIndicators.forEach(indicator => {
if (description.toLowerCase().includes(indicator)) {
score -= 0.15;
}
});
return Math.max(0, Math.min(1, score));
}
detectProhibitedElements(description) {
const detected = [];
this.prohibitedUses.forEach(prohibited => {
const keywords = this.getProhibitedKeywords(prohibited);
if (keywords.some(keyword => description.toLowerCase().includes(keyword))) {
detected.push(prohibited);
}
});
return detected;
}
getProhibitedKeywords(prohibited) {
const keywordMap = {
employee_surveillance: ['monitor employee', 'track developer', 'surveillance'],
skill_assessment_without_consent: ['evaluate skill', 'measure competence', 'rank developer'],
health_monitoring_unauthorized: ['health tracking', 'stress monitoring', 'medical assessment'],
productivity_enforcement: ['force productivity', 'mandatory efficiency', 'performance enforcement'],
discriminatory_profiling: ['profile developer', 'categorize programmer', 'demographic analysis']
};
return keywordMap[prohibited] || [];
}
generateEthicalRecommendations(score, prohibitedElements) {
const recommendations = [];
if (score < 0.7) {
recommendations.push('ユーザーの同意取得プロセスを明確化してください');
recommendations.push('データ収集の透明性を向上させてください');
recommendations.push('プライバシー保護措置を強化してください');
}
if (prohibitedElements.length > 0) {
recommendations.push('検出された問題要素を除去または修正してください');
recommendations.push('倫理委員会またはプライバシー専門家の審査を受けてください');
}
recommendations.push('定期的な倫理的影響評価を実施してください');
return recommendations;
}
}
結論
バイブコーディングは、開発者体験の革新的な向上を目指す有望な技術領域です。Chrome拡張機能として実装することで、既存のWeb開発環境に容易に統合でき、触覚フィードバックによる新しい開発者インターフェースを提供できます。
本記事で解説した技術実装は、Manifest V3に準拠したモダンな拡張機能アーキテクチャを基盤とし、認知科学的知見に基づく触覚パターン設計、機械学習による個人適応、そして包括的なアクセシビリティ対応を含む包括的なソリューションです。
しかし、この技術には明確な限界とリスクが存在します。プライバシー保護、パフォーマンス最適化、そして倫理的な利用ガイドラインの遵守が、実用的なシステム構築の前提条件となります。開発者は、これらの制約を十分に理解し、責任ある技術利用を心がける必要があります。
バイブコーディング技術の将来的な発展には、ハードウェアの進歩、Web標準の拡張、そしてアクセシビリティ技術の成熟が重要な要因となるでしょう。本記事で提供した実装フレームワークが、この新興技術領域の健全な発展に寄与することを期待します。
async initialize() { if (this.initialized) return;
// Background Serviceから設定を取得
const response = await chrome.runtime.sendMessage({ type: 'GET_SETTINGS' });
if (response.success) {
this.settings = response.data;
}
this.setupEventListeners();
this.detectCodeEditors();
this.initialized = true;
console.log('VibeCoding Content Analyzer initialized');
}
setupEventListeners() { // キーボード入力の監視 document.addEventListener(‘keydown’, this.handleKeyDown.bind(this), true); document.addEventListener(‘keyup’, this.handleKeyUp.bind(this), true);
// DOM変更の監視(動的に追加されるエディタ用)
const observer = new MutationObserver(this.handleDOMChanges.bind(this));
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
// Background Serviceからのメッセージ監視
chrome.runtime.onMessage.addListener(this.handleBackgroundMessage.bind(this));
// ページのunload時のクリーンアップ
window.addEventListener('beforeunload', this.cleanup.bind(this));
}
detectCodeEditors() { // 主要なオンラインエディタの検出パターン const editorSelectors = [ ‘.CodeMirror’, // CodeMirror ‘.monaco-editor’, // Monaco Editor (VS Code Web) ‘.ace_editor’, // Ace Editor ‘[data-testid=”code-editor”]’, // GitHub Codespaces ‘.cm-editor’, // CodeMirror 6 ‘textarea[data-language]’, // 汎用的なコードテキストエリア ];
editorSelectors.forEach(selector => {
const editors = document.querySelectorAll(selector);
editors.forEach((editor, index) => {
const editorId = `${selector}-${index}`;
if (!this.codeEditors.has(editorId)) {
this.registerCodeEditor(editorId, editor, this.detectEditorType(editor));
}
});
});
}
detectEditorType(element) { if (element.classList.contains(‘CodeMirror’)) return ‘codemirror’; if (element.classList.contains(‘monaco-editor’)) return ‘monaco’; if (element.classList.contains(‘ace_editor’)) return ‘ace’; if (element.classList.contains(‘cm-editor’)) return ‘codemirror6’; return ‘generic’; }
registerCodeEditor(id, element, type) { const editorData = { element, type, lastContent: ”, errorMarkers: [], syntaxHighlighter: null };
this.codeEditors.set(id, editorData);
this.setupEditorSpecificHooks(editorData);
console.log(`Registered code editor: ${id} (${type})`);
}
setupEditorSpecificHooks(editorData) { switch (editorData.type) { case ‘codemirror’: this.setupCodeMirrorHooks(editorData); break; case ‘monaco’: this.setupMonacoHooks(editorData); break; case ‘ace’: this.setupAceHooks(editorData); break; case ‘codemirror6’: this.setupCodeMirror6Hooks(editorData); break; default: this.setupGenericHooks(editorData); } }
setupCodeMirrorHooks(editorData) { const cm = editorData.element.CodeMirror; if (!cm) return;
// コンテンツ変更の監視
cm.on('change', (instance, changeObj) => {
this.handleContentChange(editorData, changeObj);
});
// カーソル位置変更の監視
cm.on('cursorActivity', (instance) => {
this.handleCursorActivity(editorData, instance.getCursor());
});
// エラー表示の監視
cm.on('update', (instance) => {
this.detectErrorMarkers(editorData);
});
}
setupMonacoHooks(editorData) { // Monaco Editorのインスタンス取得は少し複雑 const checkMonaco = () => { if (window.monaco && window.monaco.editor) { const editors = window.monaco.editor.getEditors(); const targetEditor = editors.find(ed => ed.getDomNode().closest(‘.monaco-editor’) === editorData.element );
if (targetEditor) {
this.attachMonacoListeners(editorData, targetEditor);
} else {
setTimeout(checkMonaco, 100);
}
} else {
setTimeout(checkMonaco, 100);
}
};
checkMonaco();
}
attachMonacoListeners(editorData, monacoInstance) { // テキスト変更の監視 monacoInstance.onDidChangeModelContent((e) => { this.handleContentChange(editorData, e); });
// カーソル位置変更の監視
monacoInstance.onDidChangeCursorPosition((e) => {
this.handleCursorActivity(editorData, e.position);
});
// マーカー(エラー)変更の監視
window.monaco.editor.onDidChangeMarkers((uris) => {
if (uris.includes(monacoInstance.getModel().uri)) {
this.detectErrorMarkers(editorData);
}
});
}
handleContentChange(editorData, changeData) { clearTimeout(this.typingTimer);
// タイピング完了後の処理(デバウンス)
this.typingTimer = setTimeout(() => {
this.analyzeCodeChange(editorData, changeData);
}, 300);
// 即座の触覚フィードバック(タイピング感)
if (this.settings.patterns.typing) {
this.triggerVibration('typing');
}
}
analyzeCodeChange(editorData, changeData) { const currentContent = this.getEditorContent(editorData); const previousContent = editorData.lastContent;
// 構文解析による変更検出
const analysis = this.performSyntaxAnalysis(currentContent, previousContent);
if (analysis.newError) {
this.triggerVibration('syntaxError');
} else if (analysis.errorFixed) {
this.triggerVibration('errorFixed');
}
// 自動補完検出
if (this.detectAutoCompletion(changeData)) {
this.triggerVibration('autoComplete');
}
editorData.lastContent = currentContent;
}
performSyntaxAnalysis(currentContent, previousContent) { // 簡単な構文エラー検出(実際の実装では言語固有のパーサーを使用) const currentErrors = this.detectSyntaxErrors(currentContent); const previousErrors = this.detectSyntaxErrors(previousContent);
return {
newError: currentErrors.length > previousErrors.length,
errorFixed: currentErrors.length < previousErrors.length,
errorCount: currentErrors.length
};
}
detectSyntaxErrors(code) { const errors = [];
// JavaScript/TypeScript基本的な構文エラー検出
const patterns = [
/\{[^}]*$/m, // 開始波括弧に対応する閉じ括弧なし
/\([^)]*$/m, // 開始丸括弧に対応する閉じ括弧なし
/\[[^\]]*$/m, // 開始角括弧に対応する閉じ括弧なし
/['"][^'"]*$/m, // 開始引用符に対応する閉じ引用符なし
/^\s*\}[^{]*$/m, // 対応する開始波括弧のない閉じ括弧
];
patterns.forEach((pattern, index) => {
const matches = code.match(pattern);
if (matches) {
errors.push({
type: `syntax_error_${index}`,
line: (code.substring(0, matches.index).match(/\n/g) || []).length + 1,
message: 'Potential syntax error detected'
});
}
});
return errors;
}
detectAutoCompletion(changeData) { // 変更データから自動補完を検出 if (changeData && changeData.text) { const insertedText = Array.isArray(changeData.text) ? changeData.text.join(”) : changeData.text;
// 典型的な自動補完パターン
const autoCompletePatterns = [
/\(\)$/, // 関数の括弧自動補完
/\{\}$/, // オブジェクトの波括弧自動補完
/\[\]$/, // 配列の角括弧自動補完
/''/, // 文字列の引用符自動補完
/""/, // 文字列のダブル引用符自動補完
];
return autoCompletePatterns.some(pattern => pattern.test(insertedText));
}
return false;
}
handleKeyDown(event) { // 特定のキーコンビネーション検出 const keyCombo = this.getKeyCombo(event); this.keySequence.push(keyCombo);
// シーケンスの長さを制限
if (this.keySequence.length > 10) {
this.keySequence.shift();
}
// 特定のパターン検出
this.detectSpecialKeyPatterns();
// デバッグ系キーの検出
if (this.isDebugKey(keyCombo)) {
this.handleDebugAction(keyCombo);
}
}
getKeyCombo(event) { const modifiers = []; if (event.ctrlKey) modifiers.push(‘Ctrl’); if (event.shiftKey) modifiers.push(‘Shift’); if (event.altKey) modifiers.push(‘Alt’); if (event.metaKey) modifiers.push(‘Meta’);
return [...modifiers, event.code].join('+');
}
isDebugKey(keyCombo) { const debugKeys = [ ‘F9’, // ブレークポイント切り替え ‘F10’, // ステップオーバー ‘F11’, // ステップイン ‘Shift+F11’, // ステップアウト ‘F5’, // 実行開始/継続 ‘Shift+F5’, // デバッグ停止 ‘Ctrl+F5’, // デバッグなしで実行 ];
return debugKeys.includes(keyCombo);
}
handleDebugAction(keyCombo) { switch (keyCombo) { case ‘F9’: this.triggerVibration(‘breakpointToggle’); break; case ‘F10’: case ‘F11’: this.triggerVibration(‘debugStep’); break; case ‘F5’: this.triggerVibration(‘debugStart’); break; case ‘Shift+F5’: this.triggerVibration(‘debugStop’); break; } }
detectSpecialKeyPatterns() { const recentKeys = this.keySequence.slice(-5);
// コナミコマンド的なパターン検出
const patterns = {
'save_sequence': ['Ctrl+KeyS', 'Ctrl+KeyS', 'Ctrl+KeyS'], // 連続保存
'undo_redo_cycle': ['Ctrl+KeyZ', 'Ctrl+KeyY', 'Ctrl+KeyZ'], // Undo-Redo サイクル
'comment_toggle': ['Ctrl+Slash', 'Ctrl+Slash'], // コメント切り替え連打
};
Object.entries(patterns).forEach(([patternName, pattern]) => {
if (this.sequenceMatches(recentKeys, pattern)) {
this.triggerVibration(patternName);
}
});
}
sequenceMatches(sequence, pattern) { if (sequence.length < pattern.length) return false;
const recentSequence = sequence.slice(-pattern.length);
return pattern.every((key, index) => recentSequence[index] === key);
}
getEditorContent(editorData) { switch (editorData.type) { case ‘codemirror’: return editorData.element.CodeMirror?.getValue() || ”; case ‘monaco’: // Monaco editorの場合は別途取得ロジックが必要 return this.getMonacoContent(editorData); case ‘ace’: return editorData.element.env?.editor?.getValue() || ”; default: return editorData.element.value || editorData.element.textContent || ”; } }
async triggerVibration(patternName) { if (!this.settings.enabled) return;
const pattern = this.settings.patterns[patternName];
if (!pattern) {
console.warn(`Unknown vibration pattern: ${patternName}`);
return;
}
// 直接振動実行
if (navigator.vibrate) {
navigator.vibrate(pattern);
}
// Background Serviceにも通知(ログ記録等のため)
chrome.runtime.sendMessage({
type: 'VIBRATION_TRIGGERED',
pattern: patternName,
timestamp: Date.now()
});
}
handleBackgroundMessage(message, sender, sendResponse) { switch (message.type) { case ‘SETTINGS_UPDATED’: this.settings = message.data; sendResponse({ success: true }); break;
case 'TEST_VIBRATION':
this.triggerVibration(message.pattern);
sendResponse({ success: true });
break;
default:
sendResponse({ success: false, error: 'Unknown message type' });
}
}
cleanup() { // イベントリスナーの削除 document.removeEventListener(‘keydown’, this.handleKeyDown.bind(this), true); document.removeEventListener(‘keyup’, this.handleKeyUp.bind(this), true);
// タイマーのクリア
if (this.typingTimer) {
clearTimeout(this.typingTimer);
}
console.log('VibeCoding Content Analyzer cleaned up');
} }
// Content Script初期化の遅延実行 if (document.readyState === ‘loading’) { document.addEventListener(‘DOMContentLoaded’, () => { new VibeCodingContentAnalyzer(); }); } else { new VibeCodingContentAnalyzer(); }
## 第3章:高度な触覚パターン設計とユーザビリティ最適化
### 3.1 認知心理学に基づく振動パターン設計
効果的な触覚フィードバックシステムの構築には、人間の触覚認知特性の深い理解が不可欠です。Weber-Fechnerの法則に従い、振動強度の知覚差異は対数的に変化するため、パターンの設計においては絶対値ではなく相対的な強度変化を重視する必要があります。
```javascript
// 高度な振動パターンジェネレータ
class HapticPatternGenerator {
constructor() {
// 人間の触覚認知特性に基づくパラメータ
this.minPerceptibleDuration = 10; // 最小知覚可能時間(ms)
this.maxComfortableDuration = 1000; // 最大快適時間(ms)
this.optimalFrequency = 250; // 最適振動周波数(Hz)の近似値
this.jndThreshold = 0.15; // Just Noticeable Difference threshold
// 感情的コンテキストマッピング
this.emotionalContexts = {
success: { baseIntensity: 0.3, rhythm: 'crescendo', mood: 'positive' },
error: { baseIntensity: 0.8, rhythm: 'staccato', mood: 'alert' },
warning: { baseIntensity: 0.5, rhythm: 'irregular', mood: 'caution' },
flow: { baseIntensity: 0.2, rhythm: 'continuous', mood: 'focus' }
};
}
generateAdaptivePattern(context, userPreferences, environmentalData) {
const basePattern = this.getBasePattern(context);
const adaptedPattern = this.applyPersonalization(basePattern, userPreferences);
const contextualPattern = this.applyEnvironmentalAdaptation(adaptedPattern, environmentalData);
return this.validateAndOptimize(contextualPattern);
}
getBasePattern(context) {
const ctx = this.emotionalContexts[context] || this.emotionalContexts.success;
switch (ctx.rhythm) {
case 'crescendo':
return this.generateCrescendoPattern(ctx.baseIntensity);
case 'staccato':
return this.generateStaccatoPattern(ctx.baseIntensity);
case 'irregular':
return this.generateIrregularPattern(ctx.baseIntensity);
case 'continuous':
return this.generateContinuousPattern(ctx.baseIntensity);
default:
return [Math.floor(ctx.baseIntensity * 200)];
}
}
generateCrescendoPattern(intensity) {
const steps = 5;
const pattern = [];
for (let i = 0; i < steps; i++) {
const currentIntensity = (intensity * (i + 1)) / steps;
const duration = Math.floor(currentIntensity * 100) + 50;
const pause = Math.floor(duration * 0.3);
pattern.push(duration);
if (i < steps - 1) pattern.push(pause);
}
return pattern;
}
generateStaccatoPattern(intensity) {
const baseDuration = Math.floor(intensity * 100) + 50;
const shortPause = Math.floor(baseDuration * 0.5);
return [baseDuration, shortPause, baseDuration, shortPause, baseDuration];
}
generateIrregularPattern(intensity) {
const baseDuration = Math.floor(intensity * 150) + 30;
const variations = [0.7, 1.3, 0.9, 1.1, 0.8];
const pattern = [];
variations.forEach((variation, index) => {
const duration = Math.floor(baseDuration * variation);
const pause = Math.floor(duration * (0.2 + Math.random() * 0.3));
pattern.push(duration);
if (index < variations.length - 1) pattern.push(pause);
});
return pattern;
}
generateContinuousPattern(intensity) {
const duration = Math.floor(intensity * 300) + 100;
return [duration];
}
applyPersonalization(pattern, preferences) {
const intensityMultiplier = preferences.intensityPreference || 1.0;
const speedMultiplier = preferences.speedPreference || 1.0;
return pattern.map((value, index) => {
if (index % 2 === 0) { // 振動時間
return Math.floor(value * intensityMultiplier);
} else { // 休止時間
return Math.floor(value / speedMultiplier);
}
});
}
applyEnvironmentalAdaptation(pattern, environmentalData) {
// 環境音レベル、時間帯、デバイス状態等による適応
let adaptationMultiplier = 1.0;
if (environmentalData.ambientNoiseLevel > 70) {
adaptationMultiplier *= 1.3; // 騒音環境では強度を上げる
}
if (environmentalData.currentHour >= 22 || environmentalData.currentHour <= 6) {
adaptationMultiplier *= 0.7; // 夜間は強度を下げる
}
if (environmentalData.batteryLevel < 20) {
adaptationMultiplier *= 0.8; // 低バッテリー時は省エネ
}
return pattern.map(value => Math.floor(value * adaptationMultiplier));
}
validateAndOptimize(pattern) {
return pattern
.map(value => Math.max(this.minPerceptibleDuration, value))
.map(value => Math.min(this.maxComfortableDuration, value))
.filter(value => value > 0);
}
}
3.2 機械学習による個人適応システム
各開発者の触覚認知特性と作業パターンに最適化された個人専用のフィードバックシステムを構築するため、機械学習アプローチを採用します。
// 個人適応型触覚学習システム
class PersonalizedHapticLearningSystem {
constructor() {
this.userInteractionHistory = [];
this.preferenceModel = {
effectivenessRatings: new Map(),
contextualPreferences: new Map(),
temporalPatterns: new Map(),
adaptationRate: 0.1
};
this.featureExtractor = new HapticFeatureExtractor();
this.decisionTree = new SimpleDecisionTree();
}
recordUserInteraction(interaction) {
const features = this.featureExtractor.extract(interaction);
const enrichedInteraction = {
...interaction,
features,
timestamp: Date.now(),
contextualFactors: this.getCurrentContextualFactors()
};
this.userInteractionHistory.push(enrichedInteraction);
this.updatePreferenceModel(enrichedInteraction);
// メモリ制限(最新1000件のみ保持)
if (this.userInteractionHistory.length > 1000) {
this.userInteractionHistory.shift();
}
}
updatePreferenceModel(interaction) {
const patternKey = this.generatePatternKey(interaction.pattern);
const contextKey = this.generateContextKey(interaction.contextualFactors);
// 効果性レーティングの更新
if (interaction.userFeedback) {
const currentRating = this.preferenceModel.effectivenessRatings.get(patternKey) || 0.5;
const newRating = currentRating + (interaction.userFeedback - currentRating) * this.preferenceModel.adaptationRate;
this.preferenceModel.effectivenessRatings.set(patternKey, newRating);
}
// コンテキスト別の好みの更新
const contextPrefs = this.preferenceModel.contextualPreferences.get(contextKey) || new Map();
const patternUsage = contextPrefs.get(patternKey) || 0;
contextPrefs.set(patternKey, patternUsage + 1);
this.preferenceModel.contextualPreferences.set(contextKey, contextPrefs);
// 時間的パターンの学習
this.updateTemporalPatterns(interaction);
}
updateTemporalPatterns(interaction) {
const timeOfDay = new Date(interaction.timestamp).getHours();
const dayOfWeek = new Date(interaction.timestamp).getDay();
const temporalKey = `${dayOfWeek}-${Math.floor(timeOfDay / 4)}`;
const patterns = this.preferenceModel.temporalPatterns.get(temporalKey) || [];
patterns.push({
pattern: interaction.pattern,
effectiveness: interaction.userFeedback || 0.5,
context: interaction.contextualFactors
});
this.preferenceModel.temporalPatterns.set(temporalKey, patterns);
}
generateOptimalPattern(context) {
const contextKey = this.generateContextKey(context);
const temporalKey = this.getCurrentTemporalKey();
// コンテキスト別の好みを取得
const contextPrefs = this.preferenceModel.contextualPreferences.get(contextKey);
// 時間的パターンを取得
const temporalPatterns = this.preferenceModel.temporalPatterns.get(temporalKey) || [];
// 決定木による最適パターン選択
const features = {
context,
temporalKey,
recentEffectiveness: this.getRecentEffectiveness(),
userActivity: this.getCurrentUserActivity()
};
return this.decisionTree.predict(features, {
contextPrefs,
temporalPatterns,
effectivenessRatings: this.preferenceModel.effectivenessRatings
});
}
getCurrentContextualFactors() {
return {
currentTime: new Date().getHours(),
dayOfWeek: new Date().getDay(),
recentErrorRate: this.calculateRecentErrorRate(),
typingSpeed: this.calculateRecentTypingSpeed(),
codeComplexity: this.estimateCurrentCodeComplexity(),
focusLevel: this.estimateFocusLevel()
};
}
calculateRecentErrorRate() {
const recentInteractions = this.userInteractionHistory
.filter(i => Date.now() - i.timestamp < 300000) // 5分以内
.filter(i => i.triggerType === 'error');
return recentInteractions.length / Math.max(1, this.userInteractionHistory.length * 0.1);
}
calculateRecentTypingSpeed() {
const recentTyping = this.userInteractionHistory
.filter(i => Date.now() - i.timestamp < 60000) // 1分以内
.filter(i => i.triggerType === 'typing');
if (recentTyping.length === 0) return 0;
const intervals = recentTyping
.map((interaction, index) => {
if (index === 0) return null;
return interaction.timestamp - recentTyping[index - 1].timestamp;
})
.filter(interval => interval !== null);
const averageInterval = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
return 60000 / averageInterval; // キー/分
}
estimateCurrentCodeComplexity() {
// 現在のコード内容から複雑度を推定
const recentCodeChanges = this.userInteractionHistory
.filter(i => Date.now() - i.timestamp < 600000) // 10分以内
.filter(i => i.codeContent);
if (recentCodeChanges.length === 0) return 0.5;
const latestCode = recentCodeChanges[recentCodeChanges.length - 1].codeContent;
// 簡単な複雑度指標
const cyclomaticComplexity = this.calculateCyclomaticComplexity(latestCode);
const nestingDepth = this.calculateNestingDepth(latestCode);
const functionCount = (latestCode.match(/function|=>/g) || []).length;
return Math.min(1.0, (cyclomaticComplexity + nestingDepth + functionCount) / 20);
}
calculateCyclomaticComplexity(code) {
const controlStructures = code.match(/if|else|while|for|switch|case|catch|\?/g) || [];
return controlStructures.length + 1;
}
calculateNestingDepth(code) {
let maxDepth = 0;
let currentDepth = 0;
for (let char of code) {
if (char === '{') {
currentDepth++;
maxDepth = Math.max(maxDepth, currentDepth);
} else if (char === '}') {
currentDepth--;
}
}
return maxDepth;
}
estimateFocusLevel() {
const recentActivity = this.userInteractionHistory
.filter(i => Date.now() - i.timestamp < 300000); // 5分以内
if (recentActivity.length === 0) return 0.5;
// アクティビティの一貫性から集中度を推定
const activityVariance = this.calculateActivityVariance(recentActivity);
const errorRate = recentActivity.filter(i => i.triggerType === 'error').length / recentActivity.length;
return Math.max(0.1, Math.min(1.0, 1.0 - activityVariance - errorRate));
}
calculateActivityVariance(activities) {
if (activities.length < 2) return 0;
const intervals = activities
.map((activity, index) => {
if (index === 0) return null;
return activity.timestamp - activities[index - 1].timestamp;
})
.filter(interval => interval !== null);
const mean = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
const variance = intervals.reduce((sum, interval) => sum + Math.pow(interval - mean, 2), 0) / intervals.length;
return Math.min(1.0, Math.sqrt(variance) / 10000); // 正規化
}
generatePatternKey(pattern) {
return Array.isArray(pattern) ? pattern.join('-') : String(pattern);
}
generateContextKey(contextualFactors) {
return [
Math.floor(contextualFactors.currentTime / 4), // 6時間単位
contextualFactors.dayOfWeek,
Math.floor(contextualFactors.codeComplexity * 10),
Math.floor(contextualFactors.focusLevel * 10)
].join('-');
}
getCurrentTemporalKey() {
const now = new Date();
return `${now.getDay()}-${Math.floor(now.getHours() / 4)}`;
}
getRecentEffectiveness() {
const recent = this.userInteractionHistory
.filter(i => Date.now() - i.timestamp < 1800000) // 30分以内
.filter(i => i.userFeedback !== undefined);
if (recent.length === 0) return 0.5;
return recent.reduce((sum, i) => sum + i.userFeedback, 0) / recent.length;
}
getCurrentUserActivity() {
const recentActivities = this.userInteractionHistory
.filter(i => Date.now() - i.timestamp < 60000) // 1分以内
.map(i => i.triggerType);
const activityCounts = recentActivities.reduce((counts, activity) => {
counts[activity] = (counts[activity] || 0) + 1;
return counts;
}, {});
return Object.keys(activityCounts).reduce((dominant, activity) =>
activityCounts[activity] > (activityCounts[dominant] || 0) ? activity : dominant
, 'idle');
}
}
// 特徴抽出器
class HapticFeatureExtractor {
extract(interaction) {
return {
patternLength: Array.isArray(interaction.pattern) ? interaction.pattern.length : 1,
totalDuration: this.calculateTotalDuration(interaction.pattern),
averageIntensity: this.calculateAverageIntensity(interaction.pattern),
rhythmType: this.classifyRhythm(interaction.pattern),
contextCategory: this.categorizeContext(interaction.context)
};
}
calculateTotalDuration(pattern) {
if (!Array.isArray(pattern)) return pattern || 0;
return pattern.reduce((sum, value) => sum + value, 0);
}
calculateAverageIntensity(pattern) {
if (!Array.isArray(pattern)) return pattern || 0;
const vibrationValues = pattern.filter((_, index) => index % 2 === 0);
return vibrationValues.reduce((sum, value) => sum + value, 0) / vibrationValues.length;
}
classifyRhythm(pattern) {
if (!Array.isArray(pattern) || pattern.length <= 2) return 'simple';
const vibrationValues = pattern.filter((_, index) => index % 2 === 0);
const variance = this.calculateVariance(vibrationValues);
if (variance < 100) return 'steady';
if (variance < 1000) return 'moderate';
return 'complex';
}
calculateVariance(values) {
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
return values.reduce((sum, value) => sum + Math.pow(value - mean, 2), 0) / values.length;
}
categorizeContext(context) {
if (context.includes('error')) return 'error';
if (context.includes('debug')) return 'debug';
if (context.includes('complete')) return 'completion';
if (context.includes('test')) return 'testing';
return 'general';
}
}
// 簡単な決定木実装
class SimpleDecisionTree {
predict(features, trainingData) {
// 簡略化された決定木ロジック
const { context, temporalKey, recentEffectiveness, userActivity } = features;
const { contextPrefs, temporalPatterns, effectivenessRatings } = trainingData;
// コンテキスト別の好みがある場合
if (contextPrefs && contextPrefs.size > 0) {
const mostUsedPattern = this.getMostUsedPattern(contextPrefs);
const effectiveness = effectivenessRatings.get(mostUsedPattern) || 0.5;
if (effectiveness > 0.7) {
return this.parsePattern(mostUsedPattern);
}
}
// 時間的パターンを考慮
if (temporalPatterns.length > 0) {
const effectiveTemporalPatterns = temporalPatterns
.filter(p => p.effectiveness > 0.6)
.sort((a, b) => b.effectiveness - a.effectiveness);
if (effectiveTemporalPatterns.length > 0) {
return effectiveTemporalPatterns[0].pattern;
}
}
// デフォルトパターン生成
return this.generateDefaultPattern(userActivity, recentEffectiveness);
}
getMostUsedPattern(contextPrefs) {
let maxUsage = 0;
let mostUsedPattern = null;
for (let [pattern, usage] of contextPrefs) {
if (usage > maxUsage) {
maxUsage = usage;
mostUsedPattern = pattern;
}
}
return mostUsedPattern;
}
parsePattern(patternKey) {
if (!patternKey) return [100];
return patternKey.split('-').map(Number).filter(n => !isNaN(n));
}
generateDefaultPattern(userActivity, recentEffectiveness) {
const baseIntensity = Math.max(50, Math.min(200, 100 * (1 + recentEffectiveness)));
switch (userActivity) {
case 'error':
return [baseIntensity * 1.5, 50, baseIntensity, 50, baseIntensity * 1.2];
case 'typing':
return [baseIntensity * 0.3];
case 'debug':
return [baseIntensity, 100, baseIntensity * 0.8];
default:
return [baseIntensity];
}
}
}
3.3 ユーザーインターフェース設計とアクセシビリティ
バイブコーディング拡張機能のユーザーインターフェースは、直感的な操作性と高いアクセシビリティを両立する必要があります。
// popup.html (拡張機能のポップアップUI)
const popupHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VibeCoding Settings</title>
<style>
body {
width: 380px;
min-height: 500px;
margin: 0;
padding: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
margin: 0;
font-size: 24px;
font-weight: 300;
}
.section {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
backdrop-filter: blur(10px);
}
.section h2 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 500;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.3);
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4CAF50;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.intensity-control {
margin: 16px 0;
}
.intensity-slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.3);
outline: none;
appearance: none;
}
.intensity-slider::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.pattern-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.pattern-item {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
font-size: 12px;
}
.pattern-item:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.pattern-item.active {
background: rgba(76, 175, 80, 0.3);
border-color: #4CAF50;
}
.test-button {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.test-button:hover {
background: rgba(255, 255, 255, 0.3);
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 12px;
}
.stat-item {
text-align: center;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.stat-value {
display: block;
font-size: 20px;
font-weight: bold;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
opacity: 0.8;
}
.accessibility-controls {
margin-top: 16px;
}
.checkbox-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.checkbox-item input[type="checkbox"] {
margin-right: 8px;
}
.checkbox-item label {
font-size: 14px;
cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
}
}
</style>
</head>
<body>
<div class="header">
<h1>🎵 VibeCoding</h1>
<p>Haptic feedback for developers</p>
</div>
<div class="section">
<h2>主要設定</h2>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>バイブコーディング有効</span>
<label class="toggle-switch">
<input type="checkbox" id="enabledToggle">
<span class="slider"></span>
</label>
</div>
<div class="intensity-control">
<label for="intensitySlider">振動強度</label>
<input type="range" id="intensitySlider" class="intensity-slider"
min="0" max="100" value="50">
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-top: 4px;">
<span>弱</span>
<span>中</span>
<span>強</span>
</div>
</div>
</div>
<div class="section">
<h2>振動パターン設定</h2>
<div class="pattern-grid" id="patternGrid">
<!-- パターンアイテムは JavaScript で動的生成 -->
</div>
</div>
<div class="section">
<h2>使用統計</h2>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="todayTriggers">0</span>
<span class="stat-label">今日のトリガー</span>
</div>
<div class="stat-item">
<span class="stat-value" id="weeklyAverage">0</span>
<span class="stat-label">週平均</span>
</div>
<div class="stat-item">
<span class="stat-value" id="mostUsedPattern">-</span>
<span class="stat-label">最頻使用パターン</span>
</div>
<div class="stat-item">
<span class="stat-value" id="effectivenessScore">0%</span>
<span class="stat-label">効果性スコア</span>
</div>
</div>
</div>
<div class="section">
<h2>アクセシビリティ</h2>
<div class="accessibility-controls">
<div class="checkbox-item">
<input type="checkbox" id="reduceMotion">
<label for="reduceMotion">アニメーション削減</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="highContrast">
<label for="highContrast">高コントラストモード</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="largeFonts">
<label for="largeFonts">大きなフォント</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="screenReaderMode">
<label for="screenReaderMode">スクリーンリーダー対応</label>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
`;
// popup.js (ポップアップの動作制御)
class VibeCodingPopupController {
constructor() {
this.settings = null;
this.statistics = null;
this.patternTypes = [
{ id: 'syntaxError', name: 'シンタックスエラー', icon: '❌', testPattern: [200, 100, 200] },
{ id: 'autoComplete', name: '自動補完', icon: '✨', testPattern: [50] },
{ id: 'breakpointHit', name: 'ブレークポイント', icon: '🔴', testPattern: [300, 200, 100] },
{ id: 'testPass', name: 'テスト成功', icon: '✅', testPattern: [100, 50, 100] },
{ id: 'testFail', name: 'テスト失敗', icon: '💥', testPattern: [150, 50, 150, 50, 150] },
{ id: 'debugStep', name: 'デバッグステップ', icon: '👣', testPattern: [80, 40, 80] }
];
this.initialize();