vibesurf 0.1.20__py3-none-any.whl → 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of vibesurf might be problematic. Click here for more details.
- vibe_surf/_version.py +2 -2
- vibe_surf/backend/api/task.py +1 -1
- vibe_surf/backend/api/voices.py +481 -0
- vibe_surf/backend/database/migrations/v004_add_voice_profiles.sql +35 -0
- vibe_surf/backend/database/models.py +38 -1
- vibe_surf/backend/database/queries.py +189 -1
- vibe_surf/backend/main.py +2 -0
- vibe_surf/backend/shared_state.py +1 -1
- vibe_surf/backend/voice_model_config.py +25 -0
- vibe_surf/browser/agen_browser_profile.py +2 -0
- vibe_surf/browser/agent_browser_session.py +3 -3
- vibe_surf/chrome_extension/background.js +224 -9
- vibe_surf/chrome_extension/content.js +147 -0
- vibe_surf/chrome_extension/manifest.json +11 -2
- vibe_surf/chrome_extension/permission-iframe.html +38 -0
- vibe_surf/chrome_extension/permission-request.html +104 -0
- vibe_surf/chrome_extension/scripts/api-client.js +61 -0
- vibe_surf/chrome_extension/scripts/main.js +8 -2
- vibe_surf/chrome_extension/scripts/permission-iframe-request.js +188 -0
- vibe_surf/chrome_extension/scripts/permission-request.js +118 -0
- vibe_surf/chrome_extension/scripts/settings-manager.js +690 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +730 -119
- vibe_surf/chrome_extension/scripts/user-settings-storage.js +422 -0
- vibe_surf/chrome_extension/scripts/voice-recorder.js +514 -0
- vibe_surf/chrome_extension/sidepanel.html +106 -29
- vibe_surf/chrome_extension/styles/components.css +35 -0
- vibe_surf/chrome_extension/styles/input.css +164 -1
- vibe_surf/chrome_extension/styles/layout.css +1 -1
- vibe_surf/chrome_extension/styles/settings-environment.css +138 -0
- vibe_surf/chrome_extension/styles/settings-forms.css +7 -7
- vibe_surf/chrome_extension/styles/variables.css +51 -0
- vibe_surf/tools/voice_asr.py +79 -8
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/METADATA +8 -12
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/RECORD +38 -31
- vibe_surf/chrome_extension/icons/convert-svg.js +0 -33
- vibe_surf/chrome_extension/icons/logo-preview.html +0 -187
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.20.dist-info → vibesurf-0.1.21.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ from typing import List, Optional, Dict, Any
|
|
|
8
8
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
9
|
from sqlalchemy import select, update, delete, func, desc, and_, or_
|
|
10
10
|
from sqlalchemy.orm import selectinload
|
|
11
|
-
from .models import Task, TaskStatus, LLMProfile, UploadedFile, McpProfile
|
|
11
|
+
from .models import Task, TaskStatus, LLMProfile, UploadedFile, McpProfile, VoiceProfile, VoiceModelType
|
|
12
12
|
from ..utils.encryption import encrypt_api_key, decrypt_api_key
|
|
13
13
|
import logging
|
|
14
14
|
import json
|
|
@@ -929,3 +929,191 @@ class UploadedFileQueries:
|
|
|
929
929
|
except Exception as e:
|
|
930
930
|
logger.error(f"Failed to cleanup deleted files: {e}")
|
|
931
931
|
raise
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
class VoiceProfileQueries:
|
|
935
|
+
"""Query operations for VoiceProfile model"""
|
|
936
|
+
|
|
937
|
+
@staticmethod
|
|
938
|
+
async def create_profile(
|
|
939
|
+
db: AsyncSession,
|
|
940
|
+
voice_profile_name: str,
|
|
941
|
+
voice_model_type: str,
|
|
942
|
+
voice_model_name: str,
|
|
943
|
+
api_key: Optional[str] = None,
|
|
944
|
+
voice_meta_params: Optional[Dict[str, Any]] = None,
|
|
945
|
+
description: Optional[str] = None
|
|
946
|
+
) -> Dict[str, Any]:
|
|
947
|
+
"""Create a new Voice profile with encrypted API key"""
|
|
948
|
+
try:
|
|
949
|
+
# Encrypt API key if provided
|
|
950
|
+
encrypted_api_key = encrypt_api_key(api_key) if api_key else None
|
|
951
|
+
|
|
952
|
+
profile = VoiceProfile(
|
|
953
|
+
voice_profile_name=voice_profile_name,
|
|
954
|
+
voice_model_type=VoiceModelType(voice_model_type),
|
|
955
|
+
voice_model_name=voice_model_name,
|
|
956
|
+
encrypted_api_key=encrypted_api_key,
|
|
957
|
+
voice_meta_params=voice_meta_params or {},
|
|
958
|
+
description=description
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
db.add(profile)
|
|
962
|
+
await db.flush()
|
|
963
|
+
await db.refresh(profile)
|
|
964
|
+
|
|
965
|
+
# Extract data immediately to avoid greenlet issues
|
|
966
|
+
profile_data = {
|
|
967
|
+
"profile_id": profile.profile_id,
|
|
968
|
+
"voice_profile_name": profile.voice_profile_name,
|
|
969
|
+
"voice_model_type": profile.voice_model_type.value,
|
|
970
|
+
"voice_model_name": profile.voice_model_name,
|
|
971
|
+
"voice_meta_params": profile.voice_meta_params,
|
|
972
|
+
"description": profile.description,
|
|
973
|
+
"is_active": profile.is_active,
|
|
974
|
+
"created_at": profile.created_at,
|
|
975
|
+
"updated_at": profile.updated_at,
|
|
976
|
+
"last_used_at": profile.last_used_at
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return profile_data
|
|
980
|
+
except Exception as e:
|
|
981
|
+
logger.error(f"Failed to create Voice profile {voice_profile_name}: {e}")
|
|
982
|
+
raise
|
|
983
|
+
|
|
984
|
+
@staticmethod
|
|
985
|
+
async def get_profile(db: AsyncSession, voice_profile_name: str) -> Optional[VoiceProfile]:
|
|
986
|
+
"""Get Voice profile by name"""
|
|
987
|
+
try:
|
|
988
|
+
result = await db.execute(
|
|
989
|
+
select(VoiceProfile).where(VoiceProfile.voice_profile_name == voice_profile_name)
|
|
990
|
+
)
|
|
991
|
+
profile = result.scalar_one_or_none()
|
|
992
|
+
if profile:
|
|
993
|
+
# Ensure all attributes are loaded by accessing them
|
|
994
|
+
_ = (profile.profile_id, profile.created_at, profile.updated_at,
|
|
995
|
+
profile.last_used_at, profile.is_active)
|
|
996
|
+
return profile
|
|
997
|
+
except Exception as e:
|
|
998
|
+
logger.error(f"Failed to get Voice profile {voice_profile_name}: {e}")
|
|
999
|
+
raise
|
|
1000
|
+
|
|
1001
|
+
@staticmethod
|
|
1002
|
+
async def get_profile_with_decrypted_key(db: AsyncSession, voice_profile_name: str) -> Optional[Dict[str, Any]]:
|
|
1003
|
+
"""Get Voice profile with decrypted API key"""
|
|
1004
|
+
try:
|
|
1005
|
+
profile = await VoiceProfileQueries.get_profile(db, voice_profile_name)
|
|
1006
|
+
if not profile:
|
|
1007
|
+
return None
|
|
1008
|
+
|
|
1009
|
+
# Decrypt API key
|
|
1010
|
+
decrypted_api_key = decrypt_api_key(profile.encrypted_api_key) if profile.encrypted_api_key else None
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
"profile_id": profile.profile_id,
|
|
1014
|
+
"voice_profile_name": profile.voice_profile_name,
|
|
1015
|
+
"voice_model_type": profile.voice_model_type.value,
|
|
1016
|
+
"voice_model_name": profile.voice_model_name,
|
|
1017
|
+
"api_key": decrypted_api_key, # Decrypted for use
|
|
1018
|
+
"voice_meta_params": profile.voice_meta_params,
|
|
1019
|
+
"description": profile.description,
|
|
1020
|
+
"is_active": profile.is_active,
|
|
1021
|
+
"created_at": profile.created_at,
|
|
1022
|
+
"updated_at": profile.updated_at,
|
|
1023
|
+
"last_used_at": profile.last_used_at
|
|
1024
|
+
}
|
|
1025
|
+
except Exception as e:
|
|
1026
|
+
logger.error(f"Failed to get Voice profile with decrypted key {voice_profile_name}: {e}")
|
|
1027
|
+
raise
|
|
1028
|
+
|
|
1029
|
+
@staticmethod
|
|
1030
|
+
async def list_profiles(
|
|
1031
|
+
db: AsyncSession,
|
|
1032
|
+
voice_model_type: Optional[str] = None,
|
|
1033
|
+
active_only: bool = True,
|
|
1034
|
+
limit: int = 50,
|
|
1035
|
+
offset: int = 0
|
|
1036
|
+
) -> List[VoiceProfile]:
|
|
1037
|
+
"""List Voice profiles"""
|
|
1038
|
+
try:
|
|
1039
|
+
query = select(VoiceProfile)
|
|
1040
|
+
|
|
1041
|
+
if active_only:
|
|
1042
|
+
query = query.where(VoiceProfile.is_active == True)
|
|
1043
|
+
|
|
1044
|
+
if voice_model_type:
|
|
1045
|
+
query = query.where(VoiceProfile.voice_model_type == VoiceModelType(voice_model_type))
|
|
1046
|
+
|
|
1047
|
+
query = query.order_by(desc(VoiceProfile.last_used_at), desc(VoiceProfile.created_at))
|
|
1048
|
+
query = query.limit(limit).offset(offset)
|
|
1049
|
+
|
|
1050
|
+
result = await db.execute(query)
|
|
1051
|
+
profiles = result.scalars().all()
|
|
1052
|
+
|
|
1053
|
+
# Ensure all attributes are loaded for each profile
|
|
1054
|
+
for profile in profiles:
|
|
1055
|
+
_ = (profile.profile_id, profile.created_at, profile.updated_at,
|
|
1056
|
+
profile.last_used_at, profile.is_active)
|
|
1057
|
+
|
|
1058
|
+
return profiles
|
|
1059
|
+
except Exception as e:
|
|
1060
|
+
logger.error(f"Failed to list Voice profiles: {e}")
|
|
1061
|
+
raise
|
|
1062
|
+
|
|
1063
|
+
@staticmethod
|
|
1064
|
+
async def update_profile(
|
|
1065
|
+
db: AsyncSession,
|
|
1066
|
+
voice_profile_name: str,
|
|
1067
|
+
updates: Dict[str, Any]
|
|
1068
|
+
) -> bool:
|
|
1069
|
+
"""Update Voice profile"""
|
|
1070
|
+
try:
|
|
1071
|
+
# Handle API key encryption if present
|
|
1072
|
+
if "api_key" in updates:
|
|
1073
|
+
api_key = updates.pop("api_key")
|
|
1074
|
+
if api_key:
|
|
1075
|
+
updates["encrypted_api_key"] = encrypt_api_key(api_key)
|
|
1076
|
+
else:
|
|
1077
|
+
updates["encrypted_api_key"] = None
|
|
1078
|
+
|
|
1079
|
+
# Handle voice_model_type enum conversion
|
|
1080
|
+
if "voice_model_type" in updates:
|
|
1081
|
+
updates["voice_model_type"] = VoiceModelType(updates["voice_model_type"])
|
|
1082
|
+
|
|
1083
|
+
result = await db.execute(
|
|
1084
|
+
update(VoiceProfile)
|
|
1085
|
+
.where(VoiceProfile.voice_profile_name == voice_profile_name)
|
|
1086
|
+
.values(**updates)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
return result.rowcount > 0
|
|
1090
|
+
except Exception as e:
|
|
1091
|
+
logger.error(f"Failed to update Voice profile {voice_profile_name}: {e}")
|
|
1092
|
+
raise
|
|
1093
|
+
|
|
1094
|
+
@staticmethod
|
|
1095
|
+
async def delete_profile(db: AsyncSession, voice_profile_name: str) -> bool:
|
|
1096
|
+
"""Delete Voice profile"""
|
|
1097
|
+
try:
|
|
1098
|
+
result = await db.execute(
|
|
1099
|
+
delete(VoiceProfile).where(VoiceProfile.voice_profile_name == voice_profile_name)
|
|
1100
|
+
)
|
|
1101
|
+
return result.rowcount > 0
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
logger.error(f"Failed to delete Voice profile {voice_profile_name}: {e}")
|
|
1104
|
+
raise
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@staticmethod
|
|
1108
|
+
async def update_last_used(db: AsyncSession, voice_profile_name: str) -> bool:
|
|
1109
|
+
"""Update the last_used_at timestamp for a profile"""
|
|
1110
|
+
try:
|
|
1111
|
+
result = await db.execute(
|
|
1112
|
+
update(VoiceProfile)
|
|
1113
|
+
.where(VoiceProfile.voice_profile_name == voice_profile_name)
|
|
1114
|
+
.values(last_used_at=func.now())
|
|
1115
|
+
)
|
|
1116
|
+
return result.rowcount > 0
|
|
1117
|
+
except Exception as e:
|
|
1118
|
+
logger.error(f"Failed to update last_used for Voice profile {voice_profile_name}: {e}")
|
|
1119
|
+
raise
|
vibe_surf/backend/main.py
CHANGED
|
@@ -20,6 +20,7 @@ from .api.files import router as files_router
|
|
|
20
20
|
from .api.activity import router as activity_router
|
|
21
21
|
from .api.config import router as config_router
|
|
22
22
|
from .api.browser import router as browser_router
|
|
23
|
+
from .api.voices import router as voices_router
|
|
23
24
|
|
|
24
25
|
# Import shared state
|
|
25
26
|
from . import shared_state
|
|
@@ -51,6 +52,7 @@ app.include_router(files_router, prefix="/api", tags=["files"])
|
|
|
51
52
|
app.include_router(activity_router, prefix="/api", tags=["activity"])
|
|
52
53
|
app.include_router(config_router, prefix="/api", tags=["config"])
|
|
53
54
|
app.include_router(browser_router, prefix="/api", tags=["browser"])
|
|
55
|
+
app.include_router(voices_router, prefix="/api", tags=["voices"])
|
|
54
56
|
|
|
55
57
|
# Global variable to control browser monitoring task
|
|
56
58
|
browser_monitor_task = None
|
|
@@ -174,7 +174,7 @@ async def execute_task_background(
|
|
|
174
174
|
db_session,
|
|
175
175
|
task_id=task_id,
|
|
176
176
|
task_result=result,
|
|
177
|
-
task_status=active_task.get("status", "completed"),
|
|
177
|
+
task_status=active_task.get("status", "completed") if active_task else "completed",
|
|
178
178
|
report_path=report_path
|
|
179
179
|
)
|
|
180
180
|
await db_session.commit()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Voice Model Configuration
|
|
3
|
+
|
|
4
|
+
Centralized configuration for all supported voice providers and their models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Voice Providers and their supported models
|
|
8
|
+
VOICE_MODELS = {
|
|
9
|
+
"qwen-asr": {
|
|
10
|
+
"model_type": "asr",
|
|
11
|
+
"requires_api_key": True,
|
|
12
|
+
"provider": "qwen",
|
|
13
|
+
},
|
|
14
|
+
"openai-asr": {
|
|
15
|
+
"model_type": "asr",
|
|
16
|
+
"requires_api_key": True,
|
|
17
|
+
"provider": "openai",
|
|
18
|
+
"supports_base_url": True,
|
|
19
|
+
},
|
|
20
|
+
"gemini-asr": {
|
|
21
|
+
"model_type": "asr",
|
|
22
|
+
"requires_api_key": True,
|
|
23
|
+
"provider": "gemini",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -17,6 +17,8 @@ from browser_use.observability import observe_debug
|
|
|
17
17
|
from browser_use.utils import _log_pretty_path, logger
|
|
18
18
|
|
|
19
19
|
from browser_use.browser import BrowserProfile
|
|
20
|
+
from browser_use.browser.profile import CHROME_DEFAULT_ARGS, CHROME_DOCKER_ARGS, CHROME_HEADLESS_ARGS, \
|
|
21
|
+
CHROME_DETERMINISTIC_RENDERING_ARGS, CHROME_DISABLE_SECURITY_ARGS, BrowserLaunchArgs
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class AgentBrowserProfile(BrowserProfile):
|
|
@@ -428,9 +428,9 @@ class AgentBrowserSession(BrowserSession):
|
|
|
428
428
|
self._popups_watchdog.attach_to_session()
|
|
429
429
|
|
|
430
430
|
# Initialize PermissionsWatchdog (handles granting and revoking browser permissions like clipboard, microphone, camera, etc.)
|
|
431
|
-
PermissionsWatchdog.model_rebuild()
|
|
432
|
-
self._permissions_watchdog = PermissionsWatchdog(event_bus=self.event_bus, browser_session=self)
|
|
433
|
-
self._permissions_watchdog.attach_to_session()
|
|
431
|
+
# PermissionsWatchdog.model_rebuild()
|
|
432
|
+
# self._permissions_watchdog = PermissionsWatchdog(event_bus=self.event_bus, browser_session=self)
|
|
433
|
+
# self._permissions_watchdog.attach_to_session()
|
|
434
434
|
|
|
435
435
|
# Initialize DefaultActionWatchdog (handles all default actions like click, type, scroll, go back, go forward, refresh, wait, send keys, upload file, scroll to text, etc.)
|
|
436
436
|
CustomActionWatchdog.model_rebuild()
|
|
@@ -155,12 +155,14 @@ class VibeSurfBackground {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
handleMessage(message, sender, sendResponse) {
|
|
158
|
+
console.log('[VibeSurf] Received message:', message.type);
|
|
158
159
|
|
|
159
160
|
// Handle async messages properly
|
|
160
161
|
(async () => {
|
|
161
162
|
try {
|
|
162
163
|
let result;
|
|
163
164
|
|
|
165
|
+
console.log('[VibeSurf] Processing message type:', message.type);
|
|
164
166
|
switch (message.type) {
|
|
165
167
|
case 'GET_CURRENT_TAB':
|
|
166
168
|
result = await this.getCurrentTabInfo();
|
|
@@ -206,9 +208,56 @@ class VibeSurfBackground {
|
|
|
206
208
|
result = await this.getAllTabs();
|
|
207
209
|
break;
|
|
208
210
|
|
|
211
|
+
case 'REQUEST_MICROPHONE_PERMISSION':
|
|
212
|
+
result = await this.requestMicrophonePermission();
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case 'REQUEST_MICROPHONE_PERMISSION_WITH_UI':
|
|
216
|
+
console.log('[VibeSurf] Handling REQUEST_MICROPHONE_PERMISSION_WITH_UI');
|
|
217
|
+
result = await this.requestMicrophonePermissionWithUI();
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'MICROPHONE_PERMISSION_RESULT':
|
|
221
|
+
console.log('[VibeSurf] Received MICROPHONE_PERMISSION_RESULT:', message);
|
|
222
|
+
console.log('[VibeSurf] Permission granted:', message.granted);
|
|
223
|
+
console.log('[VibeSurf] Permission error:', message.error);
|
|
224
|
+
|
|
225
|
+
// Handle permission result from URL parameter approach
|
|
226
|
+
if (message.granted !== undefined) {
|
|
227
|
+
console.log('[VibeSurf] Processing permission result with granted:', message.granted);
|
|
228
|
+
|
|
229
|
+
// Store the result for the original tab to retrieve
|
|
230
|
+
chrome.storage.local.set({
|
|
231
|
+
microphonePermissionResult: {
|
|
232
|
+
granted: message.granted,
|
|
233
|
+
error: message.error,
|
|
234
|
+
timestamp: Date.now()
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Also send to any waiting listeners
|
|
239
|
+
console.log('[VibeSurf] Broadcasting permission result to all tabs...');
|
|
240
|
+
chrome.runtime.sendMessage({
|
|
241
|
+
type: 'MICROPHONE_PERMISSION_RESULT',
|
|
242
|
+
granted: message.granted,
|
|
243
|
+
error: message.error
|
|
244
|
+
}).then(() => {
|
|
245
|
+
console.log('[VibeSurf] Permission result broadcast successful');
|
|
246
|
+
}).catch((err) => {
|
|
247
|
+
console.log('[VibeSurf] Permission result broadcast failed (no listeners):', err);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
result = { acknowledged: true };
|
|
251
|
+
break;
|
|
252
|
+
|
|
209
253
|
default:
|
|
210
|
-
console.warn('[VibeSurf] Unknown message type:', message.type
|
|
211
|
-
|
|
254
|
+
console.warn('[VibeSurf] Unknown message type:', message.type, 'Available handlers:', [
|
|
255
|
+
'GET_CURRENT_TAB', 'UPDATE_BADGE', 'SHOW_NOTIFICATION', 'COPY_TO_CLIPBOARD',
|
|
256
|
+
'HEALTH_CHECK', 'GET_BACKEND_STATUS', 'STORE_SESSION_DATA', 'GET_SESSION_DATA',
|
|
257
|
+
'OPEN_FILE_URL', 'OPEN_FILE_SYSTEM', 'GET_ALL_TABS', 'REQUEST_MICROPHONE_PERMISSION',
|
|
258
|
+
'REQUEST_MICROPHONE_PERMISSION_WITH_UI', 'MICROPHONE_PERMISSION_RESULT'
|
|
259
|
+
]);
|
|
260
|
+
result = { error: 'Unknown message type', receivedType: message.type };
|
|
212
261
|
}
|
|
213
262
|
|
|
214
263
|
sendResponse(result);
|
|
@@ -321,14 +370,65 @@ class VibeSurfBackground {
|
|
|
321
370
|
// Map custom types to valid Chrome notification types
|
|
322
371
|
const validType = ['basic', 'image', 'list', 'progress'].includes(type) ? type : 'basic';
|
|
323
372
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
373
|
+
// Validate icon URL and provide fallback
|
|
374
|
+
let validatedIconUrl = iconUrl;
|
|
375
|
+
try {
|
|
376
|
+
// Check if icon URL is accessible
|
|
377
|
+
if (iconUrl && iconUrl !== 'icons/icon48.png') {
|
|
378
|
+
// For custom icons, validate they exist
|
|
379
|
+
const response = await fetch(iconUrl, { method: 'HEAD' });
|
|
380
|
+
if (!response.ok) {
|
|
381
|
+
validatedIconUrl = 'icons/icon48.png';
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
// Use default icon, check if it exists
|
|
385
|
+
const defaultIconUrl = chrome.runtime.getURL('icons/icon48.png');
|
|
386
|
+
const response = await fetch(defaultIconUrl, { method: 'HEAD' });
|
|
387
|
+
if (response.ok) {
|
|
388
|
+
validatedIconUrl = defaultIconUrl;
|
|
389
|
+
} else {
|
|
390
|
+
// Fallback to logo.png if icon48.png doesn't exist
|
|
391
|
+
const logoUrl = chrome.runtime.getURL('icons/logo.png');
|
|
392
|
+
const logoResponse = await fetch(logoUrl, { method: 'HEAD' });
|
|
393
|
+
if (logoResponse.ok) {
|
|
394
|
+
validatedIconUrl = logoUrl;
|
|
395
|
+
} else {
|
|
396
|
+
// If no icons work, use empty string (browser will use default)
|
|
397
|
+
validatedIconUrl = '';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn('[VibeSurf] Icon validation failed, using fallback:', error);
|
|
403
|
+
// Use empty string as fallback (browser will use default icon)
|
|
404
|
+
validatedIconUrl = '';
|
|
405
|
+
}
|
|
330
406
|
|
|
331
|
-
|
|
407
|
+
try {
|
|
408
|
+
const notificationId = await chrome.notifications.create({
|
|
409
|
+
type: validType,
|
|
410
|
+
iconUrl: validatedIconUrl,
|
|
411
|
+
title: title || 'VibeSurf',
|
|
412
|
+
message
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return { notificationId };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error('[VibeSurf] Failed to create notification:', error);
|
|
418
|
+
// Try once more with empty icon URL
|
|
419
|
+
try {
|
|
420
|
+
const notificationId = await chrome.notifications.create({
|
|
421
|
+
type: validType,
|
|
422
|
+
iconUrl: '', // Empty string will use browser default
|
|
423
|
+
title: title || 'VibeSurf',
|
|
424
|
+
message
|
|
425
|
+
});
|
|
426
|
+
return { notificationId };
|
|
427
|
+
} catch (fallbackError) {
|
|
428
|
+
console.error('[VibeSurf] Fallback notification also failed:', fallbackError);
|
|
429
|
+
throw new Error(`Failed to create notification: ${error.message}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
332
432
|
}
|
|
333
433
|
|
|
334
434
|
async showWelcomeNotification() {
|
|
@@ -624,6 +724,121 @@ class VibeSurfBackground {
|
|
|
624
724
|
}
|
|
625
725
|
}
|
|
626
726
|
|
|
727
|
+
// Request microphone permission through background script
|
|
728
|
+
async requestMicrophonePermission() {
|
|
729
|
+
try {
|
|
730
|
+
console.log('[VibeSurf] Requesting microphone permission through background script');
|
|
731
|
+
|
|
732
|
+
// Get the active tab to inject script
|
|
733
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
734
|
+
|
|
735
|
+
if (!tab) {
|
|
736
|
+
throw new Error('No active tab found');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Check if we can inject script into this tab
|
|
740
|
+
if (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') ||
|
|
741
|
+
tab.url.startsWith('edge://') || tab.url.startsWith('moz-extension://')) {
|
|
742
|
+
throw new Error('Cannot access microphone from this type of page');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Inject script to request microphone permission
|
|
746
|
+
const results = await chrome.scripting.executeScript({
|
|
747
|
+
target: { tabId: tab.id },
|
|
748
|
+
func: () => {
|
|
749
|
+
return new Promise((resolve, reject) => {
|
|
750
|
+
try {
|
|
751
|
+
// Check if mediaDevices is available
|
|
752
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
753
|
+
reject(new Error('Media devices not supported'));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Request microphone with minimal constraints
|
|
758
|
+
const constraints = { audio: true, video: false };
|
|
759
|
+
|
|
760
|
+
navigator.mediaDevices.getUserMedia(constraints)
|
|
761
|
+
.then(stream => {
|
|
762
|
+
// Stop the stream immediately after getting permission
|
|
763
|
+
stream.getTracks().forEach(track => track.stop());
|
|
764
|
+
resolve({ success: true, hasPermission: true });
|
|
765
|
+
})
|
|
766
|
+
.catch(error => {
|
|
767
|
+
reject(new Error(`Microphone permission denied: ${error.name} - ${error.message}`));
|
|
768
|
+
});
|
|
769
|
+
} catch (error) {
|
|
770
|
+
reject(new Error(`Failed to request microphone permission: ${error.message}`));
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const result = await results[0].result;
|
|
777
|
+
console.log('[VibeSurf] Microphone permission result:', result);
|
|
778
|
+
return result;
|
|
779
|
+
|
|
780
|
+
} catch (error) {
|
|
781
|
+
console.error('[VibeSurf] Failed to request microphone permission:', error);
|
|
782
|
+
return { success: false, error: error.message };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Create a proper permission request page that opens in a new tab
|
|
787
|
+
async requestMicrophonePermissionWithUI() {
|
|
788
|
+
try {
|
|
789
|
+
console.log('[VibeSurf] Opening permission request page in new tab');
|
|
790
|
+
|
|
791
|
+
// Use the existing permission-request.html file
|
|
792
|
+
const permissionPageUrl = chrome.runtime.getURL('permission-request.html');
|
|
793
|
+
|
|
794
|
+
// Create a tab with the permission page
|
|
795
|
+
const permissionTab = await chrome.tabs.create({
|
|
796
|
+
url: permissionPageUrl,
|
|
797
|
+
active: true
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
console.log('[VibeSurf] Created permission tab:', permissionTab.id);
|
|
801
|
+
|
|
802
|
+
// Return a promise that resolves when we get the permission result
|
|
803
|
+
return new Promise((resolve) => {
|
|
804
|
+
const messageHandler = (message, sender, sendResponse) => {
|
|
805
|
+
if (message.type === 'MICROPHONE_PERMISSION_RESULT') {
|
|
806
|
+
console.log('[VibeSurf] Received permission result:', message);
|
|
807
|
+
|
|
808
|
+
// Clean up the message listener
|
|
809
|
+
chrome.runtime.onMessage.removeListener(messageHandler);
|
|
810
|
+
|
|
811
|
+
// Close the permission tab
|
|
812
|
+
chrome.tabs.remove(permissionTab.id).catch(() => {
|
|
813
|
+
// Tab might already be closed
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Resolve the promise
|
|
817
|
+
if (message.granted) {
|
|
818
|
+
resolve({ success: true, hasPermission: true });
|
|
819
|
+
} else {
|
|
820
|
+
resolve({ success: false, error: message.error || 'Permission denied by user' });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// Add the message listener
|
|
826
|
+
chrome.runtime.onMessage.addListener(messageHandler);
|
|
827
|
+
|
|
828
|
+
// Set a timeout to clean up if the tab is closed without response
|
|
829
|
+
setTimeout(() => {
|
|
830
|
+
chrome.runtime.onMessage.removeListener(messageHandler);
|
|
831
|
+
chrome.tabs.remove(permissionTab.id).catch(() => {});
|
|
832
|
+
resolve({ success: false, error: 'Permission request timed out' });
|
|
833
|
+
}, 30000); // 30 second timeout
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
} catch (error) {
|
|
837
|
+
console.error('[VibeSurf] Failed to create permission UI:', error);
|
|
838
|
+
return { success: false, error: error.message };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
627
842
|
// Cleanup method for extension unload
|
|
628
843
|
async cleanup() {
|
|
629
844
|
|