mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): implement manual login via /login command
BREAKING CHANGE: Login is no longer automatic when opening webview Changes: - Remove auto-login on webview open and restore - Add /login slash command for manual authentication - Add VSCode progress notification during login process - Add warning notification when user tries to chat without login - Implement pending message auto-retry after successful login - Add NotLoggedInMessage component (for future use) - Improve InfoBanner close button styling consistency User flow: 1. Open webview - no automatic login 2. Type /login or select from completion menu to login 3. Show "Logging in to Qwen Code..." progress notification 4. After login, show success message and auto-retry pending messages 5. If user tries to chat without login, show warning with "Login Now" button 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,11 @@ export class WebViewProvider {
|
|||||||
(message) => this.sendMessageToWebView(message),
|
(message) => this.sendMessageToWebView(message),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set login handler for /login command
|
||||||
|
this.messageHandler.setLoginHandler(async () => {
|
||||||
|
await this.initializeAgentConnection();
|
||||||
|
});
|
||||||
|
|
||||||
// Setup agent callbacks
|
// Setup agent callbacks
|
||||||
this.agentManager.onStreamChunk((chunk: string) => {
|
this.agentManager.onStreamChunk((chunk: string) => {
|
||||||
this.messageHandler.appendStreamContent(chunk);
|
this.messageHandler.appendStreamContent(chunk);
|
||||||
@@ -168,9 +173,13 @@ export class WebViewProvider {
|
|||||||
);
|
);
|
||||||
this.disposables.push(editorChangeDisposable);
|
this.disposables.push(editorChangeDisposable);
|
||||||
|
|
||||||
// Initialize agent connection only once
|
// Don't auto-login; user must use /login command
|
||||||
|
// Just initialize empty conversation for the UI
|
||||||
if (!this.agentInitialized) {
|
if (!this.agentInitialized) {
|
||||||
await this.initializeAgentConnection();
|
console.log(
|
||||||
|
'[WebViewProvider] Agent not initialized, waiting for /login command',
|
||||||
|
);
|
||||||
|
await this.initializeEmptyConversation();
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
||||||
@@ -182,9 +191,9 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize agent connection and session
|
* Initialize agent connection and session
|
||||||
* Can be called from show() or restorePanel()
|
* Can be called from show() or via /login command
|
||||||
*/
|
*/
|
||||||
private async initializeAgentConnection(): Promise<void> {
|
async initializeAgentConnection(): Promise<void> {
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|
||||||
@@ -383,14 +392,15 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
console.log('[WebViewProvider] Panel restored successfully');
|
console.log('[WebViewProvider] Panel restored successfully');
|
||||||
|
|
||||||
// Initialize agent connection if not already done
|
// Don't auto-login on restore; user must use /login command
|
||||||
|
// Just initialize empty conversation for the UI
|
||||||
if (!this.agentInitialized) {
|
if (!this.agentInitialized) {
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Initializing agent connection after restore...',
|
'[WebViewProvider] Agent not initialized after restore, waiting for /login command',
|
||||||
);
|
);
|
||||||
this.initializeAgentConnection().catch((error) => {
|
this.initializeEmptyConversation().catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
'[WebViewProvider] Failed to initialize agent after restore:',
|
'[WebViewProvider] Failed to initialize empty conversation after restore:',
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -378,7 +378,42 @@ export const App: React.FC = () => {
|
|||||||
const inputElement = inputFieldRef.current;
|
const inputElement = inputFieldRef.current;
|
||||||
const currentText = inputElement.textContent || '';
|
const currentText = inputElement.textContent || '';
|
||||||
|
|
||||||
if (item.type === 'file') {
|
if (item.type === 'command') {
|
||||||
|
// Handle /login command directly
|
||||||
|
if (item.label === '/login') {
|
||||||
|
// Clear input field
|
||||||
|
inputElement.textContent = '';
|
||||||
|
setInputText('');
|
||||||
|
// Close completion
|
||||||
|
completion.closeCompletion();
|
||||||
|
// Send login command to extension
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'login',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other commands, replace entire input with command
|
||||||
|
inputElement.textContent = item.label + ' ';
|
||||||
|
setInputText(item.label + ' ');
|
||||||
|
|
||||||
|
// Move cursor to end
|
||||||
|
setTimeout(() => {
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (inputElement.firstChild) {
|
||||||
|
range.setStart(inputElement.firstChild, (item.label + ' ').length);
|
||||||
|
range.collapse(true);
|
||||||
|
} else {
|
||||||
|
range.selectNodeContents(inputElement);
|
||||||
|
range.collapse(false);
|
||||||
|
}
|
||||||
|
sel?.removeAllRanges();
|
||||||
|
sel?.addRange(range);
|
||||||
|
inputElement.focus();
|
||||||
|
}, 10);
|
||||||
|
} else if (item.type === 'file') {
|
||||||
// Store file reference mapping
|
// Store file reference mapping
|
||||||
const filePath = (item.value as string) || item.label;
|
const filePath = (item.value as string) || item.label;
|
||||||
fileReferenceMap.current.set(item.label, filePath);
|
fileReferenceMap.current.set(item.label, filePath);
|
||||||
@@ -441,32 +476,12 @@ export const App: React.FC = () => {
|
|||||||
inputElement.focus();
|
inputElement.focus();
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
} else if (item.type === 'command') {
|
|
||||||
// Replace entire input with command
|
|
||||||
inputElement.textContent = item.label + ' ';
|
|
||||||
setInputText(item.label + ' ');
|
|
||||||
|
|
||||||
// Move cursor to end
|
|
||||||
setTimeout(() => {
|
|
||||||
const range = document.createRange();
|
|
||||||
const sel = window.getSelection();
|
|
||||||
if (inputElement.firstChild) {
|
|
||||||
range.setStart(inputElement.firstChild, (item.label + ' ').length);
|
|
||||||
range.collapse(true);
|
|
||||||
} else {
|
|
||||||
range.selectNodeContents(inputElement);
|
|
||||||
range.collapse(false);
|
|
||||||
}
|
|
||||||
sel?.removeAllRanges();
|
|
||||||
sel?.addRange(range);
|
|
||||||
inputElement.focus();
|
|
||||||
}, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close completion
|
// Close completion
|
||||||
completion.closeCompletion();
|
completion.closeCompletion();
|
||||||
},
|
},
|
||||||
[completion],
|
[completion, vscode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle attach context button click (Cmd/Ctrl + /)
|
// Handle attach context button click (Cmd/Ctrl + /)
|
||||||
@@ -642,6 +657,7 @@ export const App: React.FC = () => {
|
|||||||
// Listen for messages from extension
|
// Listen for messages from extension
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
|
// console.log('[App] Received message from extension:', message.type, message);
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'conversationLoaded': {
|
case 'conversationLoaded': {
|
||||||
@@ -714,6 +730,18 @@ export const App: React.FC = () => {
|
|||||||
setIsWaitingForResponse(false);
|
setIsWaitingForResponse(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// case 'notLoggedIn':
|
||||||
|
// // Show not logged in message with login button
|
||||||
|
// console.log('[App] Received notLoggedIn message:', message.data);
|
||||||
|
// setIsStreaming(false);
|
||||||
|
// setIsWaitingForResponse(false);
|
||||||
|
// setNotLoggedInMessage(
|
||||||
|
// (message.data as { message: string })?.message ||
|
||||||
|
// 'Please login to start chatting.',
|
||||||
|
// );
|
||||||
|
// console.log('[App] Set notLoggedInMessage to:', (message.data as { message: string })?.message);
|
||||||
|
// break;
|
||||||
|
|
||||||
case 'permissionRequest':
|
case 'permissionRequest':
|
||||||
// Show permission dialog
|
// Show permission dialog
|
||||||
handlePermissionRequest(message.data);
|
handlePermissionRequest(message.data);
|
||||||
@@ -987,6 +1015,21 @@ export const App: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a /login command
|
||||||
|
if (inputText.trim() === '/login') {
|
||||||
|
// Clear input field
|
||||||
|
setInputText('');
|
||||||
|
if (inputFieldRef.current) {
|
||||||
|
inputFieldRef.current.textContent = '';
|
||||||
|
}
|
||||||
|
// Send login command to extension
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'login',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set waiting state with random loading message
|
// Set waiting state with random loading message
|
||||||
setIsWaitingForResponse(true);
|
setIsWaitingForResponse(true);
|
||||||
setLoadingMessage(getRandomLoadingMessage());
|
setLoadingMessage(getRandomLoadingMessage());
|
||||||
@@ -1380,6 +1423,24 @@ export const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Not Logged In Message with Login Button - COMMENTED OUT */}
|
||||||
|
{/* {notLoggedInMessage && (
|
||||||
|
<>
|
||||||
|
{console.log('[App] Rendering NotLoggedInMessage with message:', notLoggedInMessage)}
|
||||||
|
<NotLoggedInMessage
|
||||||
|
message={notLoggedInMessage}
|
||||||
|
onLoginClick={() => {
|
||||||
|
setNotLoggedInMessage(null);
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'login',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDismiss={() => setNotLoggedInMessage(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)} */}
|
||||||
|
|
||||||
{isStreaming && currentStreamContent && (
|
{isStreaming && currentStreamContent && (
|
||||||
<div className="message assistant streaming">
|
<div className="message assistant streaming">
|
||||||
<div className="message-content">
|
<div className="message-content">
|
||||||
|
|||||||
@@ -287,6 +287,11 @@
|
|||||||
/* Tool Call Styles */
|
/* Tool Call Styles */
|
||||||
--app-tool-background: var(--vscode-editor-background);
|
--app-tool-background: var(--vscode-editor-background);
|
||||||
--app-code-background: var(--vscode-textCodeBlock-background);
|
--app-code-background: var(--vscode-textCodeBlock-background);
|
||||||
|
|
||||||
|
/* Warning/Error Styles */
|
||||||
|
--app-warning-background: var(--vscode-editorWarning-background, rgba(255, 204, 0, 0.1));
|
||||||
|
--app-warning-border: var(--vscode-editorWarning-foreground, #ffcc00);
|
||||||
|
--app-warning-foreground: var(--vscode-editorWarning-foreground, #ffcc00);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================
|
/* ===========================
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export class MessageHandler {
|
|||||||
}) => void;
|
}) => void;
|
||||||
// 当前消息列表
|
// 当前消息列表
|
||||||
private messages: ChatMessage[] = [];
|
private messages: ChatMessage[] = [];
|
||||||
|
// 登录处理器
|
||||||
|
private loginHandler?: () => Promise<void>;
|
||||||
|
// 待发送消息(登录后自动重发)
|
||||||
|
private pendingMessage: string | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private agentManager: QwenAgentManager,
|
private agentManager: QwenAgentManager,
|
||||||
@@ -35,6 +39,13 @@ export class MessageHandler {
|
|||||||
private sendToWebView: (message: unknown) => void,
|
private sendToWebView: (message: unknown) => void,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置登录处理器
|
||||||
|
*/
|
||||||
|
setLoginHandler(handler: () => Promise<void>): void {
|
||||||
|
this.loginHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前对话 ID
|
* 获取当前对话 ID
|
||||||
*/
|
*/
|
||||||
@@ -211,6 +222,10 @@ export class MessageHandler {
|
|||||||
await this.handleOpenSettings();
|
await this.handleOpenSettings();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'login':
|
||||||
|
await this.handleLogin();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('[MessageHandler] Unknown message type:', message.type);
|
console.warn('[MessageHandler] Unknown message type:', message.type);
|
||||||
break;
|
break;
|
||||||
@@ -316,13 +331,33 @@ export class MessageHandler {
|
|||||||
console.warn(
|
console.warn(
|
||||||
'[MessageHandler] Agent is not connected, skipping AI response',
|
'[MessageHandler] Agent is not connected, skipping AI response',
|
||||||
);
|
);
|
||||||
this.sendToWebView({
|
|
||||||
type: 'error',
|
// Save pending message for auto-retry after login
|
||||||
data: {
|
this.pendingMessage = text;
|
||||||
message:
|
console.log(
|
||||||
'Agent is not connected. Enable Qwen in settings or configure API key.',
|
'[MessageHandler] Saved pending message for retry after login',
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
// Show VSCode warning notification
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'You need to login first to use Qwen Code.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
// Trigger login
|
||||||
|
await this.handleLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMMENTED OUT: Send special error type to WebView for inline display
|
||||||
|
// console.log('[MessageHandler] Sending notLoggedIn message to webview');
|
||||||
|
// this.sendToWebView({
|
||||||
|
// type: 'notLoggedIn',
|
||||||
|
// data: {
|
||||||
|
// message: 'Please login to start chatting with Qwen Code.',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// console.log('[MessageHandler] notLoggedIn message sent');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,11 +398,44 @@ export class MessageHandler {
|
|||||||
console.log('[MessageHandler] Stream end sent');
|
console.log('[MessageHandler] Stream end sent');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[MessageHandler] Error sending message:', error);
|
console.error('[MessageHandler] Error sending message:', error);
|
||||||
vscode.window.showErrorMessage(`Error sending message: ${error}`);
|
|
||||||
this.sendToWebView({
|
// Check if error is due to no active ACP session (not logged in)
|
||||||
type: 'error',
|
const errorMsg = String(error);
|
||||||
data: { message: String(error) },
|
if (errorMsg.includes('No active ACP session')) {
|
||||||
});
|
// Save pending message for auto-retry after login
|
||||||
|
this.pendingMessage = text;
|
||||||
|
console.log(
|
||||||
|
'[MessageHandler] Saved pending message for retry after login',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show VSCode warning notification with login option
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'You need to login first to use Qwen Code.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
// Trigger login
|
||||||
|
await this.handleLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMMENTED OUT: Send special error type to WebView for inline display with login button
|
||||||
|
// console.log('[MessageHandler] Sending notLoggedIn message (session expired) to webview');
|
||||||
|
// this.sendToWebView({
|
||||||
|
// type: 'notLoggedIn',
|
||||||
|
// data: {
|
||||||
|
// message: 'Session expired. Please login to continue chatting.',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// console.log('[MessageHandler] notLoggedIn message sent');
|
||||||
|
} else {
|
||||||
|
// For other errors, show regular error message
|
||||||
|
vscode.window.showErrorMessage(`Error sending message: ${error}`);
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'error',
|
||||||
|
data: { message: errorMsg },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,4 +1014,58 @@ export class MessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理登录请求
|
||||||
|
* 通过 /login 命令触发登录流程
|
||||||
|
*/
|
||||||
|
private async handleLogin(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('[MessageHandler] Login requested via /login command');
|
||||||
|
|
||||||
|
if (this.loginHandler) {
|
||||||
|
// Show progress notification in VSCode
|
||||||
|
await vscode.window.withProgress(
|
||||||
|
{
|
||||||
|
location: vscode.ProgressLocation.Notification,
|
||||||
|
title: 'Logging in to Qwen Code...',
|
||||||
|
cancellable: false,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await this.loginHandler!();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('[MessageHandler] Login completed successfully');
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
'Successfully logged in to Qwen Code!',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-resend pending message if exists
|
||||||
|
if (this.pendingMessage) {
|
||||||
|
console.log(
|
||||||
|
'[MessageHandler] Auto-resending pending message after login',
|
||||||
|
);
|
||||||
|
const messageToSend = this.pendingMessage;
|
||||||
|
this.pendingMessage = null; // Clear pending message
|
||||||
|
|
||||||
|
// Resend the message
|
||||||
|
await this.handleSendMessage(messageToSend);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('[MessageHandler] No login handler registered');
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'error',
|
||||||
|
data: { message: 'Login handler not available' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MessageHandler] Login failed:', error);
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'error',
|
||||||
|
data: { message: `Login failed: ${error}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,11 +98,13 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
|
|||||||
|
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-center cursor-pointer p-1.5 rounded"
|
className="flex items-center justify-center cursor-pointer rounded"
|
||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
padding: '6px',
|
||||||
color: 'var(--app-secondary-foreground)',
|
color: 'var(--app-secondary-foreground)',
|
||||||
|
borderRadius: '4px',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.backgroundColor =
|
e.currentTarget.style.backgroundColor =
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
interface NotLoggedInMessageProps {
|
||||||
|
/**
|
||||||
|
* The message to display
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the login button is clicked
|
||||||
|
*/
|
||||||
|
onLoginClick: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when the message is dismissed (optional)
|
||||||
|
*/
|
||||||
|
onDismiss?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotLoggedInMessage: React.FC<NotLoggedInMessageProps> = ({
|
||||||
|
message,
|
||||||
|
onLoginClick,
|
||||||
|
onDismiss,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="flex items-start gap-3 p-4 my-4 rounded-lg"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--app-warning-background)',
|
||||||
|
borderLeft: '3px solid var(--app-warning-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Warning Icon */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="flex-shrink-0 w-5 h-5 mt-0.5"
|
||||||
|
style={{ color: 'var(--app-warning-foreground)' }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p
|
||||||
|
className="m-0 mb-3 text-sm leading-relaxed"
|
||||||
|
style={{ color: 'var(--app-primary-foreground)' }}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Login Button */}
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 text-sm font-medium rounded transition-colors duration-200"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--app-qwen-orange)',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.opacity = '0.9';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.opacity = '1';
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (onDismiss) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
onLoginClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Login Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Optional Close Button */}
|
||||||
|
{onDismiss && (
|
||||||
|
<button
|
||||||
|
className="flex-shrink-0 flex items-center justify-center cursor-pointer rounded"
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: '6px',
|
||||||
|
color: 'var(--app-secondary-foreground)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor =
|
||||||
|
'var(--app-ghost-button-hover-background)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'transparent';
|
||||||
|
}}
|
||||||
|
aria-label="Dismiss"
|
||||||
|
onClick={onDismiss}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-[10px] h-[10px]"
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 1L13 13M1 13L13 1"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user