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(),
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user