mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Merge pull request #14393 from xxoingr/fix/rail-star-before-first-save
fix(studio): a star placed before a conversation's first save stays with it
This commit is contained in:
5 files changed
+103
-5
No files matched your search
@@ -516,7 +516,7 @@ function PaneView({ port, hub, rt, title, active, visible, sideHost, side, onFoc
|
||||
<LiveWork.Provider value={live}>
|
||||
<NewAnswerAnchor.Provider value={marker ? newAnswerAt : null}>
|
||||
<Transcript port={port}
|
||||
scope={rt.sessionPath || rt.id}
|
||||
scope={rt.sessionPath}
|
||||
onSettings={onSettings}
|
||||
brought={brought}
|
||||
foldWork={foldWork}
|
||||
|
||||
@@ -99,10 +99,12 @@ describe("a star belongs to the question, not the row id", () => {
|
||||
scrollHeight: { configurable: true, value: 2400 },
|
||||
scrollTop: { configurable: true, writable: true, value: 0 },
|
||||
});
|
||||
return render(
|
||||
const rail = (s: string) => (
|
||||
<Rail marks={marks} total={marks.length} scroll={{ current: root }} flow={{ current: flow }}
|
||||
onJump={vi.fn()} bound scope={scope} />,
|
||||
onJump={vi.fn()} bound scope={s} />
|
||||
);
|
||||
const view = render(rail(scope));
|
||||
return Object.assign(view, { rescope: (s: string) => view.rerender(rail(s)) });
|
||||
};
|
||||
|
||||
it("keeps the star on the same question after a remount with fresh ids", async () => {
|
||||
@@ -143,4 +145,43 @@ describe("a star belongs to the question, not the row id", () => {
|
||||
await waitFor(() => expect(again.getAllByRole("button")[0].getAttribute("data-starred")).toBe(""));
|
||||
expect(again.getAllByRole("button")[1].getAttribute("data-starred")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a star placed before the conversation's first save", async () => {
|
||||
localStorage.clear();
|
||||
const view = draw(two("i1", "i2"), "");
|
||||
fireEvent.contextMenu(view.getAllByRole("button")[0]);
|
||||
await waitFor(() => expect(view.getAllByRole("button")[0].getAttribute("data-starred")).toBe(""));
|
||||
|
||||
view.rescope("s1");
|
||||
await waitFor(() => expect(view.getAllByRole("button")[0].getAttribute("data-starred")).toBe(""));
|
||||
view.unmount();
|
||||
|
||||
const again = draw(two("j1", "j2"), "s1");
|
||||
await waitFor(() => expect(again.getAllByRole("button")[0].getAttribute("data-starred")).toBe(""));
|
||||
expect(again.getAllByRole("button")[1].getAttribute("data-starred")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not hand an unsaved conversation's stars to the next one", async () => {
|
||||
localStorage.clear();
|
||||
const first = draw(two("i1", "i2"), "");
|
||||
fireEvent.contextMenu(first.getAllByRole("button")[0]);
|
||||
await waitFor(() => expect(first.getAllByRole("button")[0].getAttribute("data-starred")).toBe(""));
|
||||
first.unmount();
|
||||
|
||||
const next = draw(two("i1", "i2"), "");
|
||||
expect(next.getAllByRole("button")[0].getAttribute("data-starred")).toBeNull();
|
||||
});
|
||||
|
||||
it("opening a saved conversation from an unstarred draft keeps that conversation's stars", async () => {
|
||||
localStorage.clear();
|
||||
const saved = draw(two("i1", "i2"), "s1");
|
||||
fireEvent.contextMenu(saved.getAllByRole("button")[1]);
|
||||
await waitFor(() => expect(saved.getAllByRole("button")[1].getAttribute("data-starred")).toBe(""));
|
||||
saved.unmount();
|
||||
|
||||
const view = draw(two("j1", "j2"), "");
|
||||
view.rescope("s1");
|
||||
await waitFor(() => expect(view.getAllByRole("button")[1].getAttribute("data-starred")).toBe(""));
|
||||
expect(view.getAllByRole("button")[0].getAttribute("data-starred")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,8 @@ interface Props {
|
||||
flow: RefObject<HTMLDivElement | null>;
|
||||
onJump: (mark: RailMark) => void;
|
||||
/** Which conversation these marks belong to. Stars are stored per conversation,
|
||||
* because the same position in two conversations is two different questions. */
|
||||
* because the same position in two conversations is two different questions.
|
||||
* Empty until the conversation is saved: its stars are then kept in memory. */
|
||||
scope: string;
|
||||
// Only the transcript on screen answers the keys; a pane on another tab keeps
|
||||
// its rail but must not fight for them.
|
||||
@@ -70,6 +71,7 @@ function bookmarkKey(scope: string) {
|
||||
}
|
||||
|
||||
function loadBookmarks(scope: string): Set<string> {
|
||||
if (!scope) return new Set();
|
||||
try {
|
||||
const raw = localStorage.getItem(bookmarkKey(scope));
|
||||
const list = raw ? (JSON.parse(raw) as unknown) : [];
|
||||
@@ -80,6 +82,7 @@ function loadBookmarks(scope: string): Set<string> {
|
||||
}
|
||||
|
||||
function saveBookmarks(scope: string, marks: Set<string>) {
|
||||
if (!scope) return;
|
||||
try {
|
||||
localStorage.setItem(bookmarkKey(scope), JSON.stringify([...marks]));
|
||||
} catch {
|
||||
@@ -157,7 +160,19 @@ export function Rail({ marks, total, scroll, flow, onJump, bound, scope }: Props
|
||||
// Which questions the reader starred. Kept by message id so folding the rail
|
||||
// around them cannot lose one.
|
||||
const [stars, setStars] = useState<Set<string>>(() => loadBookmarks(scope));
|
||||
useEffect(() => setStars(loadBookmarks(scope)), [scope]);
|
||||
const starScope = useRef(scope);
|
||||
useEffect(() => {
|
||||
const from = starScope.current;
|
||||
starScope.current = scope;
|
||||
if (from === scope) return;
|
||||
setStars((prev) => {
|
||||
// The first save names the conversation the stars were placed in.
|
||||
if (from || !scope || prev.size === 0) return loadBookmarks(scope);
|
||||
const next = new Set([...loadBookmarks(scope), ...prev]);
|
||||
saveBookmarks(scope, next);
|
||||
return next;
|
||||
});
|
||||
}, [scope]);
|
||||
const star = useCallback((msgIndex: number | undefined) => {
|
||||
if (msgIndex === undefined) return;
|
||||
const key = String(msgIndex);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import "./testkit";
|
||||
import { Pane } from "./Pane";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { RuntimeView } from "../port/hub";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const port = new MockPort();
|
||||
port.history = async () => [
|
||||
{ role: "user", content: "第一个问题", msgIndex: 0 },
|
||||
{ role: "assistant", content: "第一段回答", msgIndex: 1 },
|
||||
{ role: "user", content: "第二个问题", msgIndex: 2 },
|
||||
{ role: "assistant", content: "第二段回答", msgIndex: 3 },
|
||||
];
|
||||
|
||||
const pane = (rt: RuntimeView) => (
|
||||
<Pane port={port} rt={rt} title="w" active visible sideHost={null} side={false}
|
||||
onFocus={() => {}} onReport={() => {}} onSessionChanged={() => {}} pulse={0} findPulse={0}
|
||||
onSettings={() => {}} needsProject={false} onOpenProject={() => {}} onKeepHere={() => {}}
|
||||
theme="dark" dockW={560} dockMax={880} onDockW={() => {}} />
|
||||
);
|
||||
|
||||
const firstMark = async () => (await screen.findAllByRole("button", { name: /定位到你说的/ }))[0];
|
||||
|
||||
it("a question starred before the first save stays starred under the saved conversation, and nowhere else", async () => {
|
||||
localStorage.clear();
|
||||
const unsaved = { id: "r1", root: "/w", name: "w" } as RuntimeView;
|
||||
const view = render(pane(unsaved));
|
||||
fireEvent.contextMenu(await firstMark());
|
||||
await waitFor(async () => expect((await firstMark()).getAttribute("data-starred")).toBe(""));
|
||||
|
||||
view.rerender(pane({ ...unsaved, sessionPath: "/w/.reasonix/sessions/a.jsonl" }));
|
||||
await waitFor(async () => expect((await firstMark()).getAttribute("data-starred")).toBe(""));
|
||||
// Runtime ids restart at r1 every launch, so a star kept under one would
|
||||
// reappear in whichever unsaved conversation that pane holds next.
|
||||
expect(Object.keys(localStorage).filter((k) => k.endsWith(":r1"))).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
- 修复:新对话第一次保存前在右侧提问细条上加的书签,保存后不再消失;也不会在重启后出现在另一段还没保存的新对话里。关联 #14296。
|
||||
- Fixed: a question starred in the rail before a new conversation is first saved stays starred once it is saved, and no longer reappears in another unsaved conversation after a restart. Refs #14296.
|
||||
Reference in new issue
Block a user