Skip to content

Conversation

@NBD-1138
Copy link

New settings options for authentication in settings>general Added error feedback and localization for auth errors, and enhances background and Live2D URL normalization to support new URL schemes and auth requirements. Refactors settings persistence and validation logic accordingly. Tested to ensure that this is working both locally and remotely and over both HTTP and HTTPS://

New settings options for authentication in settings>general
Added error feedback and localization for auth errors, and enhances
background and Live2D URL normalization to support new URL schemes and
auth requirements. Refactors settings persistence and validation logic
accordingly. Tested to ensure that this is working both locally and remotely and over both HTTP and HTTPS://
@gemini-code-assist
Copy link

Summary of Changes

Hello @NBD-1138, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request integrates basic HTTP authentication into the Electron frontend, enabling users to secure their connections to backend services. It provides a user-friendly interface in the settings for managing authentication credentials, alongside robust validation and error handling. The changes also refine how the application processes and normalizes URLs for various assets, ensuring consistent behavior across different network configurations and authentication states.

Highlights

  • Basic HTTP Authentication: Added comprehensive support for basic HTTP authentication to the Electron frontend, allowing secure connections to backend services.
  • New Settings Options: Introduced new configuration options for authentication (username, password, enable/disable) within the 'General' section of the application settings.
  • Enhanced URL Normalization: Improved the handling and normalization of URLs for background images and Live2D models, ensuring compatibility with various schemes (e.g., ws/wss to http/https) and authentication requirements.
  • Authentication Error Feedback: Implemented localized error feedback and notifications for authentication failures, guiding users to update their credentials in settings.
  • Refactored Settings & Network Logic: Overhauled settings persistence, validation, and network request interception to integrate authentication seamlessly and robustly.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces basic HTTP authentication support, which is a significant feature. The implementation touches many parts of the application, from the Electron main process to the renderer's state management and UI components. The core auth logic, including the webRequest interceptor in the main process and the fetch interceptor in the renderer, is well-designed. The refactoring of the WebSocketService to be more robust with reconnection logic is a major improvement.

However, there are several areas that need attention. The new logic for resolving background URLs in use-general-settings.ts is overly complex and should be simplified to improve maintainability. The settings page forces a page reload on save, which is a poor user experience and indicates potential issues with state management. There's also significant code duplication, particularly with a URL normalization function and what appears to be a duplicated WebSocket provider component. Additionally, the Chinese translation file is corrupted. Addressing these issues will greatly improve the quality and stability of the codebase.

Comment on lines 1 to 137
{
"common": {
"save": "保存",
"cancel": "取消",
"settings": "设置",
"close": "关闭",
"accept": "接受"
"save": "保存",
"cancel": "取消",
"settings": "设置",
"close": "关闭",
"accept": "接受"
},
"settings": {
"tabs": {
"general": "常规",
"general": "常规",
"live2d": "Live2D",
"asr": "识别",
"tts": "合成",
"agent": "代理",
"about": "关于"
"asr": "识别",
"tts": "合成",
"agent": "代理",
"about": "关于"
},
"general": {
"language": "语言",
"useCameraBackground": "使用摄像头背景",
"showSubtitle": "显示字幕",
"backgroundImage": "背景图片",
"customBgUrlPlaceholder": "输入图片URL",
"customBgUrl": "或输入自定义背景URL",
"characterPreset": "角色预设",
"wsUrl": "WebSocket地址",
"baseUrl": "基础URL",
"imageCompressionQuality": "图片压缩质量",
"imageCompressionQualityHelp": "JPEG压缩质量(0.1-1.0)。默认为0.8以减小文件体积,因为我们在向AI模型传输图片时没有进行压缩,可能导致文件过大。",
"imageMaxWidth": "图片最大宽度",
"imageMaxWidthHelp": "图片缩放的最大宽度。超过此宽度的图片会按比例缩小。设为0表示不限制大小。此功能存在的原因是某些AI模型在处理超大尺寸图片时可能存在限制。但是大部分AI模型都能自动处理图片,因此默认值为0(不限制),以保持您原始图片的完整性。"
"language": "Φ»¡Φ¿Ç",
"useCameraBackground": "使用摄像头背景",
"showSubtitle": "显示字幕",
"backgroundImage": "背景图片",
"customBgUrlPlaceholder": "输入图片URL",
"customBgUrl": "或输入自定义背景URL",
"characterPreset": "角色预设",
"wsUrl": "WebSocket地址",
"baseUrl": "基础URL",
"imageCompressionQuality": "图片压缩质量",
"imageCompressionQualityHelp": "JPEG压缩质量(0.1-1.0)。默认为0.8以减小文件体积,因为我们在向AI模型传输图片时没有进行压缩,可能导致文件过大。",
"imageMaxWidth": "图片最大宽度",
"imageMaxWidthHelp": "图片缩放的最大宽度。超过此宽度的图片会按比例缩小。设为0表示不限制大小。此功能存在的原因是某些AI模型在处理超大尺寸图片时可能存在限制。但是大部分AI模型都能自动处理图片,因此默认值为0(不限制),以保持您原始图片的完整性。"
},
"live2d": {
"pointerInteractive": "鼠标交互",
"scrollToResize": "启用滚轮缩放"
"pointerInteractive": "鼠标交互",
"scrollToResize": "启用滚轮缩放"
},
"asr": {
"autoStopMic": "AI开始说话时自动关闭麦克风",
"autoStopMicDesc": "当AI开始说话时自动关闭麦克风,防止音频反馈",
"autoStartMicOnConvEnd": "对话结束时自动开启麦克风",
"autoStartMicOnConvEndDesc": "AI说话结束时自动重新开启麦克风,实现无缝对话",
"autoStartMicOn": "AI被打断时自动开启麦克风",
"autoStartMicOnDesc": "当您打断AI时自动重新开启麦克风,保持连续交互",
"positiveSpeechThreshold": "语音识别阈值",
"positiveSpeechThresholdDesc": "检测语音所需的最低置信度(1-100),数值越高越能减少误检测",
"negativeSpeechThreshold": "负面语音阈值",
"negativeSpeechThresholdDesc": "停止语音检测的置信度下限(0-100),数值越低检测越不敏感",
"redemptionFrames": "验证帧数",
"redemptionFramesDesc": "确认语音检测所需的连续帧数(1-100),数值越高越能减少噪音触发"
"autoStopMic": "AI开始说话时自动关闭麦克风",
"autoStopMicDesc": "当AI开始说话时自动关闭麦克风,防止音频反馈",
"autoStartMicOnConvEnd": "对话结束时自动开启麦克风",
"autoStartMicOnConvEndDesc": "AI说话结束时自动重新开启麦克风,实现无缝对话",
"autoStartMicOn": "AI被打断时自动开启麦克风",
"autoStartMicOnDesc": "当您打断AI时自动重新开启麦克风,保持连续交互",
"positiveSpeechThreshold": "语音识别阈值",
"positiveSpeechThresholdDesc": "检测语音所需的最低置信度(1-100),数值越高越能减少误检测",
"negativeSpeechThreshold": "负面语音阈值",
"negativeSpeechThresholdDesc": "停止语音检测的置信度下限(0-100),数值越低检测越不敏感",
"redemptionFrames": "验证帧数",
"redemptionFramesDesc": "确认语音检测所需的连续帧数(1-100),数值越高越能减少噪音触发"
},
"agent": {
"allowProactiveSpeak": "允许AI主动发言",
"idleSecondsToSpeak": "空闲多少秒后AI可发言",
"allowButtonTrigger": "通过举手按钮提示AI发言"
"allowProactiveSpeak": "允许AI主动发言",
"idleSecondsToSpeak": "空闲多少秒后AI可发言",
"allowButtonTrigger": "通过举手按钮提示AI发言"
},
"about": {
"title": "Open LLM VTuber 前端",
"version": "版本",
"projectLinks": "项目链接",
"title": "Open LLM VTuber σëìτ½»",
"version": "τëêµ£¼",
"projectLinks": "项目链接",
"github": "GitHub",
"documentation": "文档",
"copyright": "版权",
"viewLicense": "查看许可证"
"documentation": "文档",
"copyright": "版权",
"viewLicense": "查看许可证"
}
},
"footer": {
"typeYourMessage": "输入您的消息...",
"cameraControl": "点击启动摄像头",
"cameraStopping": "点击停止摄像头",
"screenControl": "点击开始屏幕共享",
"screenStopping": "点击停止屏幕共享"
"typeYourMessage": "输入您的消息...",
"cameraControl": "点击启动摄像头",
"cameraStopping": "点击停止摄像头",
"screenControl": "点击开始屏幕共享",
"screenStopping": "点击停止屏幕共享"
},
"sidebar": {
"camera": "摄像头",
"screen": "屏幕",
"browser": "浏览器",
"live": "直播",
"noMessages": "暂无消息。开始对话吧!",
"noBrowserSession": "无活跃浏览器会话",
"browserSession": "浏览器会话"
"camera": "摄像头",
"screen": "屏幕",
"browser": "浏览器",
"live": "直播",
"noMessages": "暂无消息。开始对话吧!",
"noBrowserSession": "无活跃浏览器会话",
"browserSession": "浏览器会话"
},
"group": {
"management": "群组管理",
"yourUuid": "您的UUID",
"inviteMember": "邀请成员",
"enterMemberUuid": "输入成员UUID",
"invite": "邀请",
"members": "成员",
"you": "",
"leaveGroup": "离开群组",
"removeMember": "移除成员",
"leave": "离开"
"management": "群组管理",
"yourUuid": "您的UUID",
"inviteMember": "邀请成员",
"enterMemberUuid": "输入成员UUID",
"invite": "邀请",
"members": "成员",
"you": "您",
"leaveGroup": "离开群组",
"removeMember": "移除成员",
"leave": "离开"
},
"history": {
"chatHistoryList": "聊天历史列表",
"noMessages": "暂无消息"
"chatHistoryList": "聊天历史列表",
"noMessages": "暂无消息"
},
"notification": {
"characterLoaded": "新角色已加载",
"characterSwitched": "角色已切换",
"historyLoaded": "历史记录已加载",
"newConversation": "新对话已开始",
"newChatHistory": "新聊天历史已创建",
"historyDeleteSuccess": "历史记录删除成功",
"historyDeleteFail": "删除历史记录失败"
"characterLoaded": "新角色已加载",
"characterSwitched": "角色已切换",
"historyLoaded": "历史记录已加载",
"newConversation": "新对话已开始",
"newChatHistory": "新聊天历史已创建",
"historyDeleteSuccess": "历史记录删除成功",
"historyDeleteFail": "删除历史记录失败"
},
"error": {
"cameraApiNotSupported": "此设备不支持摄像头API",
"noCameraFound": "未找到摄像头设备",
"failedStartCamera": "启动摄像头失败",
"failedStartBackgroundCamera": "启动背景摄像头失败",
"failedStartScreenCapture": "启动屏幕捕获失败",
"failedStartVAD": "启动语音活动检测失败",
"llmCantHear": "AI无法听到您的声音",
"audioPlayback": "音频播放错误",
"enterValidUuid": "请输入有效的UUID",
"cannotDeleteCurrentHistory": "无法删除当前聊天记录",
"failedCapture": "捕获{{source}}帧失败",
"failedParseWebSocket": "解析WebSocket消息失败",
"websocketNotOpen": "WebSocket未连接",
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)"
"cameraApiNotSupported": "此设备不支持摄像头API",
"noCameraFound": "未找到摄像头设备",
"failedStartCamera": "启动摄像头失败",
"failedStartBackgroundCamera": "启动背景摄像头失败",
"failedStartScreenCapture": "启动屏幕捕获失败",
"failedStartVAD": "启动语音活动检测失败",
"llmCantHear": "AI无法听到您的声音",
"audioPlayback": "音频播放错误",
"enterValidUuid": "请输入有效的UUID",
"cannotDeleteCurrentHistory": "无法删除当前聊天记录",
"failedCapture": "捕获{{source}}帧失败",
"failedParseWebSocket": "解析WebSocket消息失败",
"websocketNotOpen": "WebSocket未连接",
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)"
},
"aiState": {
"idle": "空闲",
"thinking-speaking": "思考/说话中",
"interrupted": "已打断",
"loading": "加载中",
"listening": "聆听中",
"waiting": "等待中"
"idle": "空闲",
"thinking-speaking": "思考/说话中",
"interrupted": "已打断",
"loading": "加载中",
"listening": "聆听中",
"waiting": "等待中"
},
"wsStatus": {
"connected": "已连接",
"connecting": "连接中",
"clickToReconnect": "点击重新连接"
"connected": "已连接",
"connecting": "连接中",
"clickToReconnect": "点击重新连接",
"authFailed": "认证失败,点击重新连接"
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The content of this file appears to be corrupted, likely due to a character encoding issue. The Chinese characters are not rendered correctly (mojibake). This will break the UI for users who have selected the Chinese language. Please fix the file encoding to ensure the translations are displayed correctly.

Comment on lines +57 to +247
const arraysShallowEqual = (a: string[], b: string[]) => (
a.length === b.length && a.every((value, index) => value === b[index])
);

const settingsEqual = (prev: GeneralSettings, next: GeneralSettings) => (
arraysShallowEqual(prev.language, next.language)
&& prev.customBgUrl === next.customBgUrl
&& arraysShallowEqual(prev.selectedBgUrl, next.selectedBgUrl)
&& prev.backgroundUrl === next.backgroundUrl
&& arraysShallowEqual(prev.selectedCharacterPreset, next.selectedCharacterPreset)
&& prev.useCameraBackground === next.useCameraBackground
&& prev.wsUrl === next.wsUrl
&& prev.baseUrl === next.baseUrl
&& prev.showSubtitle === next.showSubtitle
&& prev.imageCompressionQuality === next.imageCompressionQuality
&& prev.imageMaxWidth === next.imageMaxWidth
&& prev.basicAuthEnabled === next.basicAuthEnabled
&& prev.basicAuthUsername === next.basicAuthUsername
&& prev.basicAuthPassword === next.basicAuthPassword
);

type BackgroundFile = BgUrlContextState['backgroundFiles'][number];

type BackgroundEntry = BackgroundFile | string;

const toBackgroundEntry = (value: BackgroundEntry | null | undefined): BackgroundEntry | undefined => {
if (value == null) return undefined;
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || undefined;
}
if (typeof value === 'object') {
return value;
}
return undefined;
};

const getEntryName = (entry: BackgroundEntry | undefined): string => {
if (!entry) return '';
if (typeof entry === 'string') return entry.trim();
return entry.name?.trim() || '';
};

const getEntryUrl = (entry: BackgroundEntry | undefined): string => {
if (!entry) return '';
if (typeof entry === 'string') return entry.trim();
return entry.url?.trim() || '';
};

const normalizeScheme = (value: string): string => {
if (!value) return value;
if (/^ws:\/\//i.test(value)) {
return value.replace(/^ws:\/\//i, 'http://');
}
if (/^wss:\/\//i.test(value)) {
return value.replace(/^wss:\/\//i, 'https://');
}
return value;
};

const ensureTrailingSlash = (value: string): string => (
value.endsWith('/') ? value : `${value}/`
);

const stripProtocolAndHost = (value: string): string => value.replace(/^https?:\/\/[^/]+/i, '');

const stripLeadingSlashes = (value: string): string => value.replace(/^[/\\]+/, '');

const stripBgPrefix = (value: string): string => value.replace(/^bg[/\\]/i, '');

const computeBackgroundOptionValue = (entry: BackgroundEntry): string => {
const name = getEntryName(entry);
if (name) {
return name;
}
const url = getEntryUrl(entry);
if (!url) {
return '';
}
const filename = url.split('/').filter(Boolean).pop();
return filename || url;
};

const matchBackgroundFile = (
candidateRaw: string,
files?: BackgroundEntry[],
): BackgroundEntry | undefined => {
const entries = files
?.map((value) => toBackgroundEntry(value))
.filter((value): value is BackgroundEntry => Boolean(value));

if (!entries?.length) {
return undefined;
}
const candidate = candidateRaw.trim();
if (!candidate) {
return undefined;
}

const candidateWithoutHost = stripProtocolAndHost(candidate);
const candidateWithoutLeading = stripLeadingSlashes(candidateWithoutHost);
const candidateCore = stripBgPrefix(candidateWithoutLeading);

return entries.find((entry) => {
const sources = [getEntryUrl(entry), getEntryName(entry)].filter(Boolean);
if (!sources.length) {
return false;
}
return sources.some((source) => {
const trimmedSource = source.trim();
const sourceWithoutHost = stripProtocolAndHost(trimmedSource);
const sourceWithoutLeading = stripLeadingSlashes(sourceWithoutHost);
const sourceCore = stripBgPrefix(sourceWithoutLeading);

return candidate === trimmedSource
|| candidateWithoutLeading === sourceWithoutLeading
|| candidateCore === sourceCore;
});
});
};

const normalizeBackgroundResourcePath = (
value: string,
fallbackToBgDirectory: boolean,
): string => {
const trimmed = value.trim();
if (!trimmed) {
return '';
}
if (trimmed.startsWith('/')) {
return trimmed;
}
if (trimmed.startsWith('bg/')) {
return `/${trimmed}`;
}
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
return trimmed;
}
if (fallbackToBgDirectory) {
if (trimmed.includes('/')) {
return `/${trimmed}`;
}
return `/bg/${trimmed}`;
}
return trimmed;
};

const resolveBackgroundUrl = (
candidate: string | undefined,
baseUrl: string,
files?: BackgroundEntry[],
matchedFile?: BackgroundEntry,
): string | undefined => {
const trimmedCandidate = candidate?.trim();
if (!trimmedCandidate) {
return undefined;
}
const normalizedBase = ensureTrailingSlash(normalizeScheme(baseUrl));
const entries = files
?.map((value) => toBackgroundEntry(value))
.filter((value): value is BackgroundEntry => Boolean(value));
const file = matchedFile ?? matchBackgroundFile(trimmedCandidate, entries);
const rawSource = [
getEntryUrl(file),
getEntryName(file),
trimmedCandidate,
].find((value) => Boolean(value && value.trim())) || trimmedCandidate;

const rawValue = normalizeScheme(rawSource);

if (/^https?:\/\//i.test(rawValue)) {
return rawValue;
}

const resourcePath = normalizeBackgroundResourcePath(rawValue, Boolean(file));
if (!resourcePath) {
return undefined;
}

try {
return new URL(resourcePath, normalizedBase).toString();
} catch (error) {
console.error('Failed to resolve background URL:', {
candidate: trimmedCandidate,
resourcePath,
baseUrl: normalizedBase,
error,
});
return resourcePath;
}
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for resolving background URLs is very complex and spans almost 200 lines. It involves multiple helper functions for string manipulation and matching (stripProtocolAndHost, stripLeadingSlashes, stripBgPrefix, matchBackgroundFile, etc.). This complexity makes the code difficult to understand and maintain.

A simpler approach would be to ensure that the value for each background option in the select dropdown is a stable, unique identifier (like the full URL or a unique key). This would eliminate the need for this complex and potentially brittle matching logic.

Comment on lines +477 to +481
if (changedSinceLastSave && typeof window !== 'undefined') {
window.setTimeout(() => {
window.location.reload();
}, 200);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Forcing a page reload via window.location.reload() after saving settings provides a poor user experience. It suggests that the application's state is not fully reactive to settings changes. The goal should be to update the application state dynamically without requiring a full reload. Please consider refactoring the state management so that components re-render correctly when settings are updated.

],
});

console.debug('[Settings-General] backgroundFiles from context', backgroundFiles);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file contains a console.debug statement. Please remove it before merging to keep the production console clean.

}, [setBasicAuthErrors, i18n.language]);

useEffect(() => {
console.debug('[useGeneralSettings] settings effect triggered', settings);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file contains console.debug and console.error (line 239) statements. These should be removed before merging to keep the production console clean.

NBD-1138 and others added 4 commits October 22, 2025 07:46
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Removed console debug
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant