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:
1 parent
80d2db6315
commit
50d70a5050
7 files changed
+489
-139
No files matched your search
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user