Wire up query history: localStorage session persistence + sidebar UI
This commit is contained in:
@@ -12,6 +12,28 @@ function createSessionId() {
|
||||
return `session-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
// Feature-List #25: the session id is the key `/v1/rag/history` scopes its
|
||||
// listing by, so persisting it is a prerequisite for history surviving a
|
||||
// refresh at all — not just a UX nicety. Chat MESSAGE CONTENT is
|
||||
// deliberately NOT persisted here (server never stores generated answer
|
||||
// text/blocks either, only decision/reason metadata — see
|
||||
// `PostgresTraceRepository.list_by_conversation`'s docstring): a resumed
|
||||
// session's transcript starts empty, matching the spec's own "re-run the
|
||||
// query" wording rather than "replay the old answer".
|
||||
const SESSIONS_STORAGE_KEY = "dt_sessions";
|
||||
const CURRENT_SESSION_STORAGE_KEY = "dt_current_session_id";
|
||||
|
||||
function loadStoredSessions(): ChatSession[] | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSIONS_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) && parsed.length > 0 ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ChatPage() {
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string>("");
|
||||
@@ -19,12 +41,49 @@ export default function ChatPage() {
|
||||
const [queryOverride, setQueryOverride] = useState<{ text: string; token: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = loadStoredSessions();
|
||||
if (stored) {
|
||||
const storedCurrent = (() => {
|
||||
try {
|
||||
return localStorage.getItem(CURRENT_SESSION_STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
setSessions(stored);
|
||||
setMessagesBySession(Object.fromEntries(stored.map((s) => [s.id, []])));
|
||||
setCurrentSessionId(stored.find((s) => s.id === storedCurrent)?.id ?? stored[0].id);
|
||||
return;
|
||||
}
|
||||
const id = createSessionId();
|
||||
setSessions([{ id, title: "Phiên tra cứu mới", updatedAt: new Date().toISOString() }]);
|
||||
setMessagesBySession({ [id]: [] });
|
||||
setCurrentSessionId(id);
|
||||
}, []);
|
||||
|
||||
// Persist whenever the session list / active session changes — covers
|
||||
// new/deleted sessions and switching between them. Guarded on non-empty
|
||||
// so the pre-mount-effect empty state never overwrites a real stored
|
||||
// list with `[]`.
|
||||
useEffect(() => {
|
||||
if (sessions.length === 0) return;
|
||||
try {
|
||||
localStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(sessions));
|
||||
} catch {
|
||||
// Storage full or unavailable (e.g. private browsing) — the session
|
||||
// list simply won't survive a refresh this time.
|
||||
}
|
||||
}, [sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentSessionId) return;
|
||||
try {
|
||||
localStorage.setItem(CURRENT_SESSION_STORAGE_KEY, currentSessionId);
|
||||
} catch {
|
||||
// ignore — same fallback as above
|
||||
}
|
||||
}, [currentSessionId]);
|
||||
|
||||
// Citation & Evidence Panel State
|
||||
const [citations, setCitations] = useState<Citation[]>([]);
|
||||
const [activeCitationIndex, setActiveCitationIndex] = useState<number | null>(null);
|
||||
|
||||
Reference in New Issue
Block a user