完善用户登录态的感知

This commit is contained in:
2025-11-12 15:07:24 +08:00
parent 7b8d31f899
commit 787906c566
4 changed files with 477 additions and 43 deletions
+310 -26
View File
@@ -1,15 +1,22 @@
import React, { useState, useEffect, useRef } from 'react'
import { Input, Button, Typography, Space, Modal, message } from 'antd'
import { SendOutlined, CommentOutlined } from '@ant-design/icons'
import { Input, Button, Typography, Space, Modal, message, Drawer, Skeleton } from 'antd'
import { SendOutlined, CommentOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'
const { TextArea } = Input
const { Text } = Typography
interface MessageMetadata {
type: 'article' | 'chat'
articleUrl?: string
originalUserInput?: string // 用于重新生成
}
interface Message {
id: string
role: 'user' | 'assistant'
content: string
timestamp: Date
metadata?: MessageMetadata
}
interface ModelConfig {
@@ -34,12 +41,18 @@ const Chat: React.FC = () => {
const [isLoading, setIsLoading] = useState(false)
const [currentArticleUrl, setCurrentArticleUrl] = useState<string>('')
const [lastAiResponse, setLastAiResponse] = useState<string>('')
const [isPostingComment, setIsPostingComment] = useState(false)
const [isQrModalVisible, setIsQrModalVisible] = useState(false)
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>('')
const [qrCodeError, setQrCodeError] = useState<string>('')
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false)
const [confirmUsername, setConfirmUsername] = useState<string>('')
const [regeneratingMessageId, setRegeneratingMessageId] = useState<string | null>(null)
const [isAccountDrawerVisible, setIsAccountDrawerVisible] = useState(false)
const [isCheckingLoginStatus, setIsCheckingLoginStatus] = useState(false)
const [xiaoheiheLoginStatus, setXiaoheiheLoginStatus] = useState<{
isLoggedIn: boolean
username?: string
}>({ isLoggedIn: false })
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollTimerRef = useRef<NodeJS.Timeout | null>(null)
const messagesContainerRef = useRef<HTMLDivElement>(null)
@@ -96,10 +109,14 @@ const Chat: React.FC = () => {
let finalContent = input
// Store article URL in a local variable for immediate use
let articleUrl = ''
// If URL detected, try to fetch article content using Playwright
if (urls && urls.length > 0) {
const url = urls[0]
setCurrentArticleUrl(url) // 保存文章 URL
articleUrl = url // 保存到局部变量以便立即使用
setCurrentArticleUrl(url) // 保存文章 URL 到状态
setLastAiResponse('') // 清空之前的 AI 回复
isArticleRequestRef.current = true // 标记这是文章请求
@@ -222,7 +239,10 @@ const Chat: React.FC = () => {
id: Date.now().toString(),
role: 'user',
content: finalContent,
timestamp: new Date()
timestamp: new Date(),
metadata: isArticleRequestRef.current
? { type: 'article', originalUserInput: input }
: { type: 'chat', originalUserInput: input }
}
setMessages((prev) => [...prev, userMessage])
@@ -235,7 +255,14 @@ const Chat: React.FC = () => {
id: assistantId,
role: 'assistant',
content: '',
timestamp: new Date()
timestamp: new Date(),
metadata: isArticleRequestRef.current
? {
type: 'article',
articleUrl: articleUrl, // 使用局部变量而不是状态变量
originalUserInput: input
}
: { type: 'chat', originalUserInput: input }
}
setMessages((prev) => [...prev, assistantMessage])
@@ -371,6 +398,130 @@ const Chat: React.FC = () => {
}
}
// 重新生成消息
const handleRegenerateMessage = async (messageId: string): Promise<void> => {
// 找到要重新生成的消息
const messageIndex = messages.findIndex((msg) => msg.id === messageId)
if (messageIndex === -1) return
const messageToRegenerate = messages[messageIndex]
if (!messageToRegenerate.metadata?.originalUserInput) {
message.error('无法重新生成:缺少原始输入')
return
}
// 找到对应的用户消息(前一条消息)
const userMessageIndex = messageIndex - 1
if (userMessageIndex < 0) return
setRegeneratingMessageId(messageId)
// 清空当前消息内容,显示加载状态
setMessages((prev) =>
prev.map((msg) => (msg.id === messageId ? { ...msg, content: '' } : msg))
)
try {
// 获取模型配置
const savedConfigs = localStorage.getItem('ai-model-configs')
const activeConfigId = localStorage.getItem('active-ai-model')
if (!savedConfigs || !activeConfigId) {
throw new Error('请先在设置中配置 AI 模型')
}
const configs = JSON.parse(savedConfigs)
const activeConfig = configs.find((c: ModelConfig) => c.id === activeConfigId)
if (!activeConfig) {
throw new Error('未找到活动的模型配置')
}
// 构建对话历史(只包含重新生成之前的消息)
const conversationHistory = messages.slice(0, userMessageIndex + 1).map((msg) => ({
role: msg.role,
content: msg.content
}))
// 调用 AI API
const response = await fetch(`${activeConfig.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${activeConfig.apiKey}`
},
body: JSON.stringify({
model: activeConfig.model,
messages: conversationHistory,
stream: true
})
})
if (!response.ok) {
throw new Error(`API 请求失败: ${response.statusText}`)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('无法读取响应流')
}
let accumulatedContent = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
const lines = chunk.split('\n').filter((line) => line.trim() !== '')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6)
if (data === '[DONE]') continue
try {
const parsed = JSON.parse(data)
const content = parsed.choices[0]?.delta?.content
if (content) {
accumulatedContent += content
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId ? { ...msg, content: accumulatedContent } : msg
)
)
}
} catch (e) {
console.error('Error parsing SSE data:', e)
}
}
}
}
// 如果是文章请求,更新 lastAiResponse
if (messageToRegenerate.metadata?.type === 'article') {
setLastAiResponse(accumulatedContent)
setCurrentArticleUrl(messageToRegenerate.metadata.articleUrl || '')
}
message.success('重新生成成功')
} catch (error) {
console.error('Regenerate error:', error)
message.error(error instanceof Error ? error.message : '重新生成失败')
// 恢复原内容
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId ? { ...msg, content: messageToRegenerate.content } : msg
)
)
} finally {
setRegeneratingMessageId(null)
}
}
const showQrCodeLogin = async (): Promise<void> => {
console.log('showQrCodeLogin called')
@@ -413,9 +564,17 @@ const Chat: React.FC = () => {
message.success(`登录成功!欢迎 ${result.username}`)
setIsQrModalVisible(false)
// 登录成功后,直接显示确认发送对话框
console.log('Showing confirm dialog with username:', result.username)
showConfirmDialog(result.username)
// 更新登录状态
setXiaoheiheLoginStatus({
isLoggedIn: true,
username: result.username
})
// 只有在有待发送评论时才显示确认对话框
if (currentArticleUrl && lastAiResponse) {
console.log('Showing confirm dialog with username:', result.username)
showConfirmDialog(result.username)
}
} else {
console.log('Login failed:', result.error)
message.error(result.error || '登录失败')
@@ -441,7 +600,6 @@ const Chat: React.FC = () => {
console.log('lastAiResponse length:', lastAiResponse?.length)
setIsConfirmModalVisible(false)
setIsPostingComment(true)
message.loading({ content: '正在发送评论...', key: 'posting', duration: 0 })
try {
@@ -467,7 +625,6 @@ const Chat: React.FC = () => {
message.error(error instanceof Error ? error.message : '发送评论时出错')
} finally {
console.log('handleConfirmOk finally block')
setIsPostingComment(false)
}
}
@@ -481,6 +638,47 @@ const Chat: React.FC = () => {
setQrCodeError('')
}
// 检查小黑盒登录状态
const checkXiaoheiheLoginStatus = async (): Promise<void> => {
setIsCheckingLoginStatus(true)
try {
// 使用快速检查方法(仅基于 cookie,不加载页面)
const result = await window.electron.ipcRenderer.invoke(
'check-platform-login-fast',
'https://www.xiaoheihe.cn/app/bbs/home'
)
if (result.success) {
setXiaoheiheLoginStatus({
isLoggedIn: result.isLoggedIn,
username: result.username
})
}
} catch (error) {
console.error('Check login status error:', error)
} finally {
setIsCheckingLoginStatus(false)
}
}
const handleLogout = async (): Promise<void> => {
try {
message.loading({ content: '正在退出登录...', key: 'logout', duration: 0 })
const result = await window.electron.ipcRenderer.invoke('logout-platform', 'xiaoheihe')
message.destroy('logout')
if (result.success) {
setXiaoheiheLoginStatus({ isLoggedIn: false })
message.success('已退出登录')
} else {
message.error(result.error || '退出登录失败')
}
} catch (error) {
message.destroy('logout')
message.error(error instanceof Error ? error.message : '退出登录失败')
}
}
const handlePostComment = async (): Promise<void> => {
console.log('handlePostComment called')
console.log('currentArticleUrl:', currentArticleUrl)
@@ -545,12 +743,25 @@ const Chat: React.FC = () => {
padding: '16px 24px',
background: '#fff',
borderBottom: '1px solid #e8e8e8',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<Text strong style={{ fontSize: '16px' }}>
AI
</Text>
<Button
icon={<SettingOutlined />}
onClick={() => {
setIsAccountDrawerVisible(true)
// 打开 Drawer 时检查登录状态
checkXiaoheiheLoginStatus()
}}
>
</Button>
</div>
{/* Messages List */}
@@ -623,6 +834,36 @@ const Chat: React.FC = () => {
minute: '2-digit'
})}
</div>
{/* AI 消息的操作按钮 */}
{message.role === 'assistant' && message.content && (
<div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
<Button
size="small"
icon={<ReloadOutlined />}
onClick={() => handleRegenerateMessage(message.id)}
loading={regeneratingMessageId === message.id}
disabled={regeneratingMessageId !== null}
>
</Button>
{message.metadata?.type === 'article' && message.metadata.articleUrl && (
<Button
size="small"
type="primary"
icon={<CommentOutlined />}
onClick={() => {
// 更新当前文章 URL 和 AI 回复
setCurrentArticleUrl(message.metadata!.articleUrl!)
setLastAiResponse(message.content)
handlePostComment()
}}
>
</Button>
)}
</div>
)}
</div>
</div>
))}
@@ -661,20 +902,6 @@ const Chat: React.FC = () => {
</Button>
</Space.Compact>
{/* 发送评论按钮 */}
{currentArticleUrl && lastAiResponse && (
<Button
type="default"
icon={<CommentOutlined />}
onClick={handlePostComment}
disabled={isPostingComment}
loading={isPostingComment}
style={{ width: '100%' }}
>
AI
</Button>
)}
</Space>
</div>
</div>
@@ -748,6 +975,63 @@ const Chat: React.FC = () => {
</div>
</div>
</Modal>
{/* Account Management Drawer */}
<Drawer
title="账号管理"
placement="right"
onClose={() => setIsAccountDrawerVisible(false)}
open={isAccountDrawerVisible}
width={400}
>
<div>
<div style={{ marginBottom: 24 }}>
<Text strong style={{ fontSize: 16 }}>
</Text>
<div
style={{
marginTop: 12,
padding: 16,
background: '#f5f5f5',
borderRadius: 8
}}
>
{isCheckingLoginStatus ? (
<Skeleton active paragraph={{ rows: 2 }} />
) : (
<>
<div style={{ marginBottom: 8 }}>
<Text type="secondary"></Text>
<Text strong style={{ marginLeft: 8 }}>
{xiaoheiheLoginStatus.isLoggedIn ? '已登录' : '未登录'}
</Text>
</div>
{xiaoheiheLoginStatus.isLoggedIn && xiaoheiheLoginStatus.username && (
<div style={{ marginBottom: 8 }}>
<Text type="secondary"></Text>
<Text strong style={{ marginLeft: 8 }}>
{xiaoheiheLoginStatus.username}
</Text>
</div>
)}
<div style={{ marginTop: 12 }}>
{xiaoheiheLoginStatus.isLoggedIn ? (
<Button type="default" block danger onClick={handleLogout}>
退
</Button>
) : (
<Button type="primary" block onClick={showQrCodeLogin}>
</Button>
)}
</div>
</>
)}
</div>
</div>
</div>
</Drawer>
</>
)
}