fix: seperate static QR code and dynamic spin components (#327)

* fix: seperate static QR code and dynamic spin components

* fix: format issues
This commit is contained in:
Mingholy
2025-08-14 21:17:56 +08:00
committed by GitHub
parent c33d162ff2
commit 1ffcb51052

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { Box, Text, useInput, Static } from 'ink'; import { Box, Text, useInput } from 'ink';
import Spinner from 'ink-spinner'; import Spinner from 'ink-spinner';
import Link from 'ink-link'; import Link from 'ink-link';
import qrcode from 'qrcode-terminal'; import qrcode from 'qrcode-terminal';
@@ -26,17 +26,93 @@ interface QwenOAuthProgressProps {
authMessage?: string | null; authMessage?: string | null;
} }
interface StaticItem { /**
key: string; * Static QR Code Display Component
type: * Renders the QR code and URL once and doesn't re-render unless the URL changes
| 'title' */
| 'instructions' function QrCodeDisplay({
| 'url' verificationUrl,
| 'qr-instructions' qrCodeData,
| 'qr-code' }: {
| 'auth-content'; verificationUrl: string;
url?: string; qrCodeData: string | null;
qrCode?: string; }): React.JSX.Element | null {
if (!qrCodeData) {
return null;
}
return (
<Box
borderStyle="round"
borderColor={Colors.AccentBlue}
flexDirection="column"
padding={1}
width="100%"
>
<Text bold color={Colors.AccentBlue}>
Qwen OAuth Authentication
</Text>
<Box marginTop={1}>
<Text>Please visit this URL to authorize:</Text>
</Box>
<Link url={verificationUrl} fallback={false}>
<Text color={Colors.AccentGreen} bold>
{verificationUrl}
</Text>
</Link>
<Box marginTop={1}>
<Text>Or scan the QR code below:</Text>
</Box>
<Box marginTop={1}>
<Text>{qrCodeData}</Text>
</Box>
</Box>
);
}
/**
* Dynamic Status Display Component
* Shows the loading spinner, timer, and status messages
*/
function StatusDisplay({
timeRemaining,
dots,
}: {
timeRemaining: number;
dots: string;
}): React.JSX.Element {
const formatTime = (seconds: number): string => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
return (
<Box
borderStyle="round"
borderColor={Colors.AccentBlue}
flexDirection="column"
padding={1}
width="100%"
>
<Box marginTop={1}>
<Text>
<Spinner type="dots" /> Waiting for authorization{dots}
</Text>
</Box>
<Box marginTop={1} justifyContent="space-between">
<Text color={Colors.Gray}>
Time remaining: {formatTime(timeRemaining)}
</Text>
<Text color={Colors.AccentPurple}>(Press ESC to cancel)</Text>
</Box>
</Box>
);
} }
export function QwenOAuthProgress({ export function QwenOAuthProgress({
@@ -60,15 +136,12 @@ export function QwenOAuthProgress({
} }
}); });
// Generate QR code when device auth is available // Generate QR code once when device auth is available
useEffect(() => { useEffect(() => {
if (!deviceAuth) { if (!deviceAuth?.verification_uri_complete) {
setQrCodeData(null);
return; return;
} }
// Only generate QR code if we don't have one yet for this URL
if (qrCodeData === null) {
const generateQR = () => { const generateQR = () => {
try { try {
qrcode.generate( qrcode.generate(
@@ -85,8 +158,7 @@ export function QwenOAuthProgress({
}; };
generateQR(); generateQR();
} }, [deviceAuth?.verification_uri_complete]);
}, [deviceAuth, qrCodeData]);
// Countdown timer // Countdown timer
useEffect(() => { useEffect(() => {
@@ -115,11 +187,17 @@ export function QwenOAuthProgress({
return () => clearInterval(dotsTimer); return () => clearInterval(dotsTimer);
}, []); }, []);
const formatTime = (seconds: number): string => { // Memoize the QR code display to prevent unnecessary re-renders
const minutes = Math.floor(seconds / 60); const qrCodeDisplay = useMemo(() => {
const remainingSeconds = seconds % 60; if (!deviceAuth?.verification_uri_complete) return null;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}; return (
<QrCodeDisplay
verificationUrl={deviceAuth.verification_uri_complete}
qrCodeData={qrCodeData}
/>
);
}, [deviceAuth?.verification_uri_complete, qrCodeData]);
// Handle timeout state // Handle timeout state
if (authStatus === 'timeout') { if (authStatus === 'timeout') {
@@ -151,6 +229,7 @@ export function QwenOAuthProgress({
); );
} }
// Show loading state when no device auth is available yet
if (!deviceAuth) { if (!deviceAuth) {
return ( return (
<Box <Box
@@ -167,7 +246,8 @@ export function QwenOAuthProgress({
</Box> </Box>
<Box marginTop={1} justifyContent="space-between"> <Box marginTop={1} justifyContent="space-between">
<Text color={Colors.Gray}> <Text color={Colors.Gray}>
Time remaining: {formatTime(timeRemaining)} Time remaining: {Math.floor(timeRemaining / 60)}:
{(timeRemaining % 60).toString().padStart(2, '0')}
</Text> </Text>
<Text color={Colors.AccentPurple}>(Press ESC to cancel)</Text> <Text color={Colors.AccentPurple}>(Press ESC to cancel)</Text>
</Box> </Box>
@@ -176,77 +256,12 @@ export function QwenOAuthProgress({
} }
return ( return (
<> <Box flexDirection="column" width="100%">
{qrCodeData && ( {/* Static QR Code Display */}
<Static {qrCodeDisplay}
items={
[
{
key: 'auth-content',
type: 'auth-content' as const,
url: deviceAuth.verification_uri_complete,
qrCode: qrCodeData,
},
] as StaticItem[]
}
style={{
width: '100%',
}}
>
{(item: StaticItem) => (
<Box
borderStyle="round"
borderColor={Colors.AccentBlue}
flexDirection="column"
padding={1}
width="100%"
key={item.key}
>
<Text bold color={Colors.AccentBlue}>
Qwen OAuth Authentication
</Text>
<Box marginTop={1}> {/* Dynamic Status Display */}
<Text>Please visit this URL to authorize:</Text> <StatusDisplay timeRemaining={timeRemaining} dots={dots} />
</Box> </Box>
<Link url={item.url || ''} fallback={false}>
<Text color={Colors.AccentGreen} bold>
{item.url || ''}
</Text>
</Link>
<Box marginTop={1}>
<Text>Or scan the QR code below:</Text>
</Box>
<Box marginTop={1}>
<Text>{item.qrCode || ''}</Text>
</Box>
</Box>
)}
</Static>
)}
<Box
borderStyle="round"
borderColor={Colors.AccentBlue}
flexDirection="column"
padding={1}
width="100%"
>
<Box marginTop={1}>
<Text>
<Spinner type="dots" /> Waiting for authorization{dots}
</Text>
</Box>
<Box marginTop={1} justifyContent="space-between">
<Text color={Colors.Gray}>
Time remaining: {formatTime(timeRemaining)}
</Text>
<Text color={Colors.AccentPurple}>(Press ESC to cancel)</Text>
</Box>
</Box>
</>
); );
} }