完善用户登录态的感知
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user