feat: 更新时间戳处理逻辑,支持从消息中提取并格式化时间,同时为话题列表添加分页功能

This commit is contained in:
ooodc 2026-06-07 18:17:13 +08:00
parent 7e8b6a832e
commit 62ea6de3a7
3 changed files with 78 additions and 11 deletions

View File

@ -718,7 +718,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
role: msg.role.clone(), role: msg.role.clone(),
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(msg.timestamp / 1000),
}); });
} }
} }
@ -730,7 +730,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
attachments: Vec::new(), attachments: Vec::new(),
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(msg.timestamp / 1000),
}) })
} }
"tool" => { "tool" => {
@ -745,7 +745,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
duration_ms: msg.tool_duration_ms, duration_ms: msg.tool_duration_ms,
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(msg.timestamp / 1000),
}), }),
ToolMessageState::PendingUserAction => Some(WsOutbound::ToolPending { ToolMessageState::PendingUserAction => Some(WsOutbound::ToolPending {
id: msg.id.clone(), id: msg.id.clone(),
@ -756,7 +756,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(), resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(),
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(msg.timestamp / 1000),
}), }),
} }
} }
@ -767,7 +767,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
attachments, attachments,
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(msg.timestamp / 1000),
}), }),
_ => None, _ => None,
} }

View File

@ -76,7 +76,7 @@ function getFileName(path: string): string {
} }
function formatTime(timestamp: number) { function formatTime(timestamp: number) {
return new Date(timestamp).toLocaleTimeString('zh-CN', { return new Date(timestamp * 1000).toLocaleTimeString('zh-CN', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}) })

View File

@ -1,5 +1,5 @@
import { useState } from 'react' import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import { Plus, MessageSquare, Layers, Hash, Clock, RefreshCw, Trash2, Check, X } from 'lucide-react' import { Plus, MessageSquare, Layers, Hash, Clock, RefreshCw, Trash2, Check, X, ChevronLeft, ChevronRight } from 'lucide-react'
import type { Topic } from '../../types/protocol' import type { Topic } from '../../types/protocol'
interface TopicListProps { interface TopicListProps {
@ -41,6 +41,50 @@ export function TopicList({
}: TopicListProps) { }: TopicListProps) {
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null) const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
// Pagination — dynamically sized to fill one screen without scrolling
const ESTIMATED_ITEM_HEIGHT = 64 // py-3(24px) + title(20px) + mt-1.5(6px) + meta(14px)
const LIST_PADDING = 24 // p-3 top + bottom
const [pageSize, setPageSize] = useState(8) // fallback before measurement
const [currentPage, setCurrentPage] = useState(0)
const listRef = useRef<HTMLDivElement>(null)
const measurePageSize = useCallback(() => {
const el = listRef.current
if (!el) return
const available = el.clientHeight - LIST_PADDING
setPageSize(Math.max(1, Math.floor(available / ESTIMATED_ITEM_HEIGHT) - 1))
}, [])
useEffect(() => {
measurePageSize()
const el = listRef.current
if (!el) return
const observer = new ResizeObserver(() => measurePageSize())
observer.observe(el)
return () => observer.disconnect()
}, [measurePageSize])
const totalPages = useMemo(
() => Math.max(1, Math.ceil(topics.length / pageSize)),
[topics.length, pageSize]
)
const pagedTopics = useMemo(
() => topics.slice(currentPage * pageSize, (currentPage + 1) * pageSize),
[topics, currentPage, pageSize]
)
// Reset page when topics list changes (e.g., new data loaded)
useEffect(() => {
setCurrentPage(0)
}, [topics])
// Clamp currentPage when it exceeds totalPages (e.g., after deletion on last page)
useEffect(() => {
if (currentPage >= totalPages) {
setCurrentPage(Math.max(0, totalPages - 1))
}
}, [currentPage, totalPages])
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{/* Header */} {/* Header */}
@ -83,7 +127,7 @@ export function TopicList({
</div> </div>
{/* Topics 列表 */} {/* Topics 列表 */}
<div className="flex-1 overflow-y-auto p-3"> <div ref={listRef} className="flex-1 overflow-y-auto p-3">
{!sessionId ? ( {!sessionId ? (
<div className="p-4 text-center text-sm text-[var(--text-muted)]"> <div className="p-4 text-center text-sm text-[var(--text-muted)]">
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" /> <MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
@ -97,7 +141,7 @@ export function TopicList({
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{topics.map((topic, index) => ( {pagedTopics.map((topic, index) => (
<div key={topic.id} className="group relative"> <div key={topic.id} className="group relative">
<button <button
onClick={() => onSwitchTopic(topic.id)} onClick={() => onSwitchTopic(topic.id)}
@ -109,7 +153,7 @@ export function TopicList({
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="mt-0.5 text-xs text-[var(--text-muted)] font-mono w-4"> <span className="mt-0.5 text-xs text-[var(--text-muted)] font-mono w-4">
{index + 1} {currentPage * pageSize + index + 1}
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className={`truncate font-medium ${ <div className={`truncate font-medium ${
@ -179,6 +223,29 @@ export function TopicList({
</div> </div>
)} )}
</div> </div>
{/* Pagination bar */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 border-t border-[var(--border-color)] px-3 py-2">
<button
onClick={() => setCurrentPage(p => Math.max(0, p - 1))}
disabled={currentPage === 0}
className="flex items-center justify-center h-7 w-7 rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-subtle)] disabled:text-[var(--text-muted)] disabled:cursor-not-allowed transition-all"
>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-xs text-[var(--text-muted)] min-w-[3rem] text-center select-none">
{currentPage + 1} / {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages - 1, p + 1))}
disabled={currentPage >= totalPages - 1}
className="flex items-center justify-center h-7 w-7 rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-subtle)] disabled:text-[var(--text-muted)] disabled:cursor-not-allowed transition-all"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
)}
</div> </div>
) )
} }