feat: 更新时间戳处理逻辑,支持从消息中提取并格式化时间,同时为话题列表添加分页功能
This commit is contained in:
parent
7e8b6a832e
commit
62ea6de3a7
@ -718,7 +718,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
|
||||
role: msg.role.clone(),
|
||||
subagent_task_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(),
|
||||
subagent_task_id: None,
|
||||
topic_id: None,
|
||||
timestamp: Some(crate::protocol::now_timestamp()),
|
||||
timestamp: Some(msg.timestamp / 1000),
|
||||
})
|
||||
}
|
||||
"tool" => {
|
||||
@ -745,7 +745,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
|
||||
subagent_task_id: None,
|
||||
topic_id: None,
|
||||
duration_ms: msg.tool_duration_ms,
|
||||
timestamp: Some(crate::protocol::now_timestamp()),
|
||||
timestamp: Some(msg.timestamp / 1000),
|
||||
}),
|
||||
ToolMessageState::PendingUserAction => Some(WsOutbound::ToolPending {
|
||||
id: msg.id.clone(),
|
||||
@ -756,7 +756,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
|
||||
resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(),
|
||||
subagent_task_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,
|
||||
subagent_task_id: None,
|
||||
topic_id: None,
|
||||
timestamp: Some(crate::protocol::now_timestamp()),
|
||||
timestamp: Some(msg.timestamp / 1000),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ function getFileName(path: string): string {
|
||||
}
|
||||
|
||||
function formatTime(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleTimeString('zh-CN', {
|
||||
return new Date(timestamp * 1000).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Plus, MessageSquare, Layers, Hash, Clock, RefreshCw, Trash2, Check, X } from 'lucide-react'
|
||||
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
||||
import { Plus, MessageSquare, Layers, Hash, Clock, RefreshCw, Trash2, Check, X, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import type { Topic } from '../../types/protocol'
|
||||
|
||||
interface TopicListProps {
|
||||
@ -41,6 +41,50 @@ export function TopicList({
|
||||
}: TopicListProps) {
|
||||
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 (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
@ -83,7 +127,7 @@ export function TopicList({
|
||||
</div>
|
||||
|
||||
{/* Topics 列表 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<div ref={listRef} className="flex-1 overflow-y-auto p-3">
|
||||
{!sessionId ? (
|
||||
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
|
||||
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
@ -97,7 +141,7 @@ export function TopicList({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{topics.map((topic, index) => (
|
||||
{pagedTopics.map((topic, index) => (
|
||||
<div key={topic.id} className="group relative">
|
||||
<button
|
||||
onClick={() => onSwitchTopic(topic.id)}
|
||||
@ -109,7 +153,7 @@ export function TopicList({
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="mt-0.5 text-xs text-[var(--text-muted)] font-mono w-4">
|
||||
{index + 1}
|
||||
{currentPage * pageSize + index + 1}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`truncate font-medium ${
|
||||
@ -179,6 +223,29 @@ export function TopicList({
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user