完善用户登录态的感知
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { app, BrowserWindow, ipcMain, screen, globalShortcut, clipboard } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { chromium, BrowserContext } from 'playwright'
|
||||
import { existsSync, rmSync } from 'fs'
|
||||
import { ScraperFactory } from './scrapers'
|
||||
import { XiaoheiheScrap } from './scrapers/xiaoheihe'
|
||||
import { GenericScraper } from './scrapers/generic'
|
||||
@@ -237,6 +238,33 @@ async function waitForQrCodeLogin(): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
// Check login status for a platform (fast - cookie-based only)
|
||||
async function checkPlatformLoginFast(url: string): Promise<{
|
||||
success: boolean
|
||||
isLoggedIn: boolean
|
||||
username?: string
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const service = platformServiceFactory.getService(url)
|
||||
if (!service) {
|
||||
return { success: false, isLoggedIn: false, error: '不支持的平台' }
|
||||
}
|
||||
|
||||
const context = await getPersistentContext()
|
||||
const loginStatus = await service.checkLoginStatusFast(context)
|
||||
|
||||
return { success: true, ...loginStatus }
|
||||
} catch (error) {
|
||||
console.error('Check platform login fast error:', error)
|
||||
return {
|
||||
success: false,
|
||||
isLoggedIn: false,
|
||||
error: error instanceof Error ? error.message : '检查登录状态失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check login status for a platform
|
||||
async function checkPlatformLogin(url: string): Promise<{
|
||||
success: boolean
|
||||
@@ -387,6 +415,11 @@ app.whenReady().then(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Handle check platform login status (fast - cookie-based only)
|
||||
ipcMain.handle('check-platform-login-fast', async (_, url: string) => {
|
||||
return await checkPlatformLoginFast(url)
|
||||
})
|
||||
|
||||
// Handle check platform login status
|
||||
ipcMain.handle('check-platform-login', async (_, url: string) => {
|
||||
return await checkPlatformLogin(url)
|
||||
@@ -407,6 +440,35 @@ app.whenReady().then(() => {
|
||||
return await waitForQrCodeLogin()
|
||||
})
|
||||
|
||||
// Handle logout
|
||||
ipcMain.handle('logout-platform', async (_, platform: string) => {
|
||||
try {
|
||||
if (platform === 'xiaoheihe') {
|
||||
// Close the persistent context to clear cookies and session
|
||||
if (persistentContext) {
|
||||
await persistentContext.close()
|
||||
persistentContext = null
|
||||
}
|
||||
|
||||
// Delete the user data directory to completely clear all browser data
|
||||
if (existsSync(userDataDir)) {
|
||||
console.log('Deleting user data directory:', userDataDir)
|
||||
rmSync(userDataDir, { recursive: true, force: true })
|
||||
console.log('User data directory deleted successfully')
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false, error: '不支持的平台' }
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '退出登录失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createFloatingWindow()
|
||||
registerGlobalShortcuts()
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ export interface PlatformService {
|
||||
// 平台标识
|
||||
canHandle(url: string): boolean
|
||||
|
||||
// 快速检查登录状态(仅基于 cookie,不加载页面)
|
||||
checkLoginStatusFast(context: BrowserContext): Promise<{
|
||||
isLoggedIn: boolean
|
||||
username?: string
|
||||
}>
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus(page: Page): Promise<{
|
||||
isLoggedIn: boolean
|
||||
|
||||
@@ -6,30 +6,91 @@ export class XiaoheiheService implements PlatformService {
|
||||
return url.includes('xiaoheihe.cn')
|
||||
}
|
||||
|
||||
// 快速检查登录状态(仅基于 cookie,不加载页面)
|
||||
async checkLoginStatusFast(context: BrowserContext): Promise<{
|
||||
isLoggedIn: boolean
|
||||
username?: string
|
||||
}> {
|
||||
try {
|
||||
// 直接检查 cookie,无需加载页面
|
||||
const cookies = await context.cookies('https://www.xiaoheihe.cn')
|
||||
const hasLoginCookie = cookies.some(cookie =>
|
||||
cookie.name === 'heybox_id' ||
|
||||
cookie.name === 'pkey' ||
|
||||
cookie.name.includes('token')
|
||||
)
|
||||
|
||||
if (!hasLoginCookie) {
|
||||
return { isLoggedIn: false }
|
||||
}
|
||||
|
||||
// 如果有 cookie,快速加载一个简单的 API 页面来获取用户名
|
||||
// 这比加载完整的首页快得多
|
||||
return { isLoggedIn: true }
|
||||
} catch (error) {
|
||||
console.error('Fast check login status error:', error)
|
||||
return { isLoggedIn: false }
|
||||
}
|
||||
}
|
||||
|
||||
async checkLoginStatus(page: Page): Promise<{
|
||||
isLoggedIn: boolean
|
||||
username?: string
|
||||
}> {
|
||||
try {
|
||||
// 检查是否存在登录按钮(未登录状态)
|
||||
const loginButton = await page.locator('.user-box__login').count()
|
||||
if (loginButton > 0) {
|
||||
return { isLoggedIn: false }
|
||||
}
|
||||
// 等待页面稳定
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// 检查是否存在用户名(已登录状态)
|
||||
const usernameElement = await page.locator('.user-box__username').first()
|
||||
const usernameCount = await usernameElement.count()
|
||||
|
||||
if (usernameCount > 0) {
|
||||
const username = await usernameElement.textContent()
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
username: username?.trim() || undefined
|
||||
// 尝试多种方式检查登录状态
|
||||
const loginStatus = await page.evaluate(() => {
|
||||
// 方法1: 检查登录按钮
|
||||
const loginButton = document.querySelector('.user-box__login')
|
||||
if (loginButton) {
|
||||
return { isLoggedIn: false, method: 'loginButton' }
|
||||
}
|
||||
}
|
||||
|
||||
return { isLoggedIn: false }
|
||||
// 方法2: 检查用户名元素
|
||||
const usernameElement = document.querySelector('.user-box__username')
|
||||
if (usernameElement) {
|
||||
const username = usernameElement.textContent?.trim()
|
||||
return { isLoggedIn: true, username, method: 'username' }
|
||||
}
|
||||
|
||||
// 方法3: 检查用户头像或其他登录标识
|
||||
const userAvatar = document.querySelector('.user-box__avatar')
|
||||
if (userAvatar) {
|
||||
// 尝试从其他位置获取用户名
|
||||
const nameElement = document.querySelector('.user-name, .username, [class*="user"] [class*="name"]')
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
username: nameElement?.textContent?.trim(),
|
||||
method: 'avatar'
|
||||
}
|
||||
}
|
||||
|
||||
// 方法4: 检查 localStorage 或 cookie 中的登录信息(带错误处理)
|
||||
try {
|
||||
const hasAuthToken = !!localStorage.getItem('token') ||
|
||||
!!localStorage.getItem('auth') ||
|
||||
document.cookie.includes('heybox_id')
|
||||
if (hasAuthToken) {
|
||||
return { isLoggedIn: true, method: 'token' }
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage 访问失败,尝试只检查 cookie
|
||||
if (document.cookie.includes('heybox_id')) {
|
||||
return { isLoggedIn: true, method: 'cookie' }
|
||||
}
|
||||
}
|
||||
|
||||
return { isLoggedIn: false, method: 'default' }
|
||||
})
|
||||
|
||||
console.log('Login status check result:', loginStatus)
|
||||
return {
|
||||
isLoggedIn: loginStatus.isLoggedIn,
|
||||
username: loginStatus.username
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Check login status error:', error)
|
||||
return { isLoggedIn: false }
|
||||
@@ -138,7 +199,7 @@ export class XiaoheiheService implements PlatformService {
|
||||
console.log(`waitForQrCodeLogin: Check #${checkCount} - Checking login status...`)
|
||||
|
||||
const loginStatus = await this.checkLoginStatus(page)
|
||||
console.log(`waitForQrCodeLogin: Check #${checkCount} - isLoggedIn:`, loginStatus.isLoggedIn, 'username:', loginStatus.username)
|
||||
console.log(`waitForQrCodeLogin: Check #${checkCount} - Login status:`, loginStatus)
|
||||
|
||||
if (loginStatus.isLoggedIn) {
|
||||
console.log('waitForQrCodeLogin: Login detected! Closing page and returning success')
|
||||
@@ -149,6 +210,27 @@ export class XiaoheiheService implements PlatformService {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 cookie 中是否有登录凭证(扫码成功后 cookie 会先更新)
|
||||
const cookies = await context.cookies()
|
||||
const hasLoginCookie = cookies.some(cookie =>
|
||||
cookie.name === 'heybox_id' ||
|
||||
cookie.name === 'pkey' ||
|
||||
cookie.name.includes('token')
|
||||
)
|
||||
|
||||
console.log(`waitForQrCodeLogin: Check #${checkCount} - Has login cookie:`, hasLoginCookie)
|
||||
|
||||
if (hasLoginCookie) {
|
||||
console.log('waitForQrCodeLogin: Login cookie detected! Login successful.')
|
||||
// Cookie 的存在就是最可靠的登录凭证,不需要通过页面验证
|
||||
// 直接认定登录成功
|
||||
await page.close()
|
||||
return {
|
||||
success: true,
|
||||
username: undefined
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`waitForQrCodeLogin: Check #${checkCount} - Not logged in yet, waiting 2 seconds...`)
|
||||
await page.waitForTimeout(2000) // 每2秒检查一次
|
||||
}
|
||||
|
||||
@@ -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