feat: 对话列表支持分页、收藏、置顶

收藏(is_favorite)/置顶(is_pinned)作为布尔标记存入对话已有 metadata,
复用现有 PATCH /api/threads/{id} 与 search 往返,无需后端改动与迁移。

- hooks: 新增 useUpdateThreadMetadata + useToggleThreadPin/Favorite
- utils: 新增 isThreadPinned/isThreadFavorite/sortThreadsPinnedFirst
- 侧边栏: 置顶优先排序 + 下拉菜单收藏/置顶项 + 置顶图标
- 对话页: 置顶区 + 全部/收藏过滤 + 页码翻页(每页20) + 行内收藏/置顶按钮
- i18n: zh-CN/en-US/types 新增相关文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-08 18:01:03 +08:00
1 parent 80d2db6315
commit 50d70a5050
7 files changed
+489 -139

No files matched your search

+116 -36
View File
@@ -621,15 +621,15 @@ export function useThreadStream({
lockThreadRagMode(threadId);
}
const mergedContext = {
...context,
...extraContext,
};
await thread.submit(
{
messages: [
{
const mergedContext = {
...context,
...extraContext,
};
await thread.submit(
{
messages: [
{
type: "human",
content: [
{
@@ -647,33 +647,33 @@ export function useThreadStream({
},
],
},
{
threadId: threadId,
streamSubgraphs: true,
streamResumable: true,
config: {
recursion_limit: 1000,
},
context: {
...mergedContext,
thinking_enabled: mergedContext.mode !== "flash",
is_plan_mode:
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
subagent_enabled: mergedContext.mode === "ultra",
reasoning_effort:
mergedContext.reasoning_effort ??
(mergedContext.mode === "ultra"
? "high"
: mergedContext.mode === "pro"
? "medium"
: mergedContext.mode === "thinking"
? "low"
: undefined),
memory_injection_enabled:
mergedContext.memory_injection_enabled ?? true,
rag_mode_enabled: ragModeEnabled,
thread_id: threadId,
},
{
threadId: threadId,
streamSubgraphs: true,
streamResumable: true,
config: {
recursion_limit: 1000,
},
context: {
...mergedContext,
thinking_enabled: mergedContext.mode !== "flash",
is_plan_mode:
mergedContext.mode === "pro" || mergedContext.mode === "ultra",
subagent_enabled: mergedContext.mode === "ultra",
reasoning_effort:
mergedContext.reasoning_effort ??
(mergedContext.mode === "ultra"
? "high"
: mergedContext.mode === "pro"
? "medium"
: mergedContext.mode === "thinking"
? "low"
: undefined),
memory_injection_enabled:
mergedContext.memory_injection_enabled ?? true,
rag_mode_enabled: ragModeEnabled,
thread_id: threadId,
},
},
);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
@@ -853,6 +853,86 @@ export function useDeleteMessage() {
});
}
/**
* Merge metadata into a conversation's record via the Gateway
* `PATCH /api/threads/{id}` route. Used to persist per-conversation flags such
* as `is_pinned` (置顶) and `is_favorite` (收藏) — a conversation belongs to a
* single user, so these live directly on the thread metadata (no join table).
* Updates the `["threads", "search"]` cache optimistically so the UI reflects
* the toggle immediately, mirroring `useRenameThread`.
*/
export function useUpdateThreadMetadata() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
threadId,
metadata,
}: {
threadId: string;
metadata: Record<string, unknown>;
}) => {
const response = await apiFetch(
`${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ metadata }),
},
);
if (!response.ok) {
const error = await response
.json()
.catch(() => ({ detail: "更新对话失败。" }));
throw new Error(error.detail ?? "更新对话失败。");
}
},
onSuccess(_, { threadId, metadata }) {
queryClient.setQueriesData(
{
queryKey: ["threads", "search"],
exact: false,
},
(oldData: Array<AgentThread> | undefined) => {
if (oldData == null) {
return oldData;
}
return oldData.map((t) =>
t.thread_id === threadId
? {
...t,
metadata: {
...(t.metadata as Record<string, unknown> | null),
...metadata,
},
}
: t,
);
},
);
},
});
}
/** Toggle a conversation's pinned (置顶) flag. */
export function useToggleThreadPin() {
const { mutate, ...rest } = useUpdateThreadMetadata();
return {
...rest,
toggle: (threadId: string, pinned: boolean) =>
mutate({ threadId, metadata: { is_pinned: pinned } }),
};
}
/** Toggle a conversation's favorite (收藏) flag. */
export function useToggleThreadFavorite() {
const { mutate, ...rest } = useUpdateThreadMetadata();
return {
...rest,
toggle: (threadId: string, favorite: boolean) =>
mutate({ threadId, metadata: { is_favorite: favorite } }),
};
}
export function useRenameThread() {
const queryClient = useQueryClient();
const apiClient = getAPIClient();