fix(chat): 修复会话刷新丢失最终答案(#3) + 反向分页/详情懒加载/会话URL持久化

- 后端消息列表默认按 id 升序取最旧一页,长会话刷新后最终答案落在页外而丢失
  (发新消息推进游标才重现)。改为:
  - db.ConvActivityPage:反向分页,取最新/更早一页 + hasMore
  - 消息接口按 since(增量)/before(更早页)/最新页 三种模式返回
- 前端聊天页:
  - 打开会话仅加载最新一页,滚动到顶部再反向加载更早(保持滚动位置不跳)
  - Transcript 的 UserRow/AnswerBlock 改为进入视口才拉 detail,避免打开会话即对
    整页每步发详情请求
  - 选中会话 id 持久化到 URL(?c=<id>),刷新回到当前会话
  - 修复新建会话后被"清理失效 id"effect 误清导致停留在新建页、输入框内容与灰色
    发送按钮卡死:移除该 effect + onStarted 乐观插入

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 5 committed 2026-08-07 11:45:33 -04:00
1 parent 349b0d104d
commit 5000aadd8f
5 files changed
+207 -17

No files matched your search

+10
View File
@@ -275,10 +275,20 @@ export const api = {
updateConversationProfile: (id: number, llm_profile_id: number | null) =>
patch<{ ok: boolean }>(`/conversations/${id}/profile`, { llm_profile_id }),
deleteConversation: (id: number) => del<{ deleted: number }>(`/conversations/${id}`),
// Incremental tail: steps after `since` (id ASC) — live poll + post-send fetch.
conversationMessages: (id: number, since = 0) =>
get<{ items: Activity[]; cursor: number; running: boolean }>(`/conversations/${id}/messages?since=${since}`).then(
(r) => ({ items: arr(r.items), cursor: r.cursor ?? 0, running: !!r.running }),
),
// Reverse pagination: the latest `limit` steps (before=0) or the page of older
// steps ending before id `before`. hasMore = still-older steps exist.
conversationHistory: (id: number, before = 0, limit = 200) => {
const q = new URLSearchParams({ limit: String(limit) });
if (before > 0) q.set("before", String(before));
return get<{ items: Activity[]; cursor: number; running: boolean; hasMore: boolean }>(
`/conversations/${id}/messages?${q.toString()}`,
).then((r) => ({ items: arr(r.items), cursor: r.cursor ?? 0, running: !!r.running, hasMore: !!r.hasMore }));
},
conversationMsgDetail: (id: number, seq: number) =>
get<{ detail: string }>(`/conversations/${id}/messages/${seq}`).then((r) => r.detail ?? ""),
sendConversationMessage: (id: number, message: string) =>