refactor(nexus): migrate chat state off legacy panel context
Phase 9 flagged that ChatMessageList, ChatConversationList, and PersonalAssistant all read activeConversationId / scrollToMessageId from the legacy ChatPanelContext, blocking deletion of the old desktop chat drawer. Phase 16a migrates those reads off context using prop-drilling through HistorySheet so PersonalAssistant owns a local useState for the active conversation id. - ChatMessageList accepts optional scrollToMessageId/onScrollComplete props in place of useChatPanel(). - ChatConversationList accepts activeConversationId + onSelectConversation so HistorySheet can pass them through. - HistorySheet forwards the new props to its embedded list. - PersonalAssistant owns activeConversationId as local state and wires HistorySheet to it. - NexusOnboardingWizard drops its setChatOpen(true) call — the Assistant page is the chat surface now, no drawer to toggle. Leaves ChatPanel.tsx and MobileChatView.tsx as the only remaining useChatPanel consumers; both are dead chrome slated for deletion in the next commit of this phase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb76b5eeef
commit
1b6727bb1c
6 changed files with 134 additions and 27 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { GitBranch, Plus, Search, X } from "lucide-react";
|
import { GitBranch, Plus, Search, X } from "lucide-react";
|
||||||
import { useChatConversations } from "../hooks/useChatConversations";
|
import { useChatConversations } from "../hooks/useChatConversations";
|
||||||
import { useChatPanel } from "../context/ChatPanelContext";
|
|
||||||
import { useMediaQuery } from "../hooks/useMediaQuery";
|
import { useMediaQuery } from "../hooks/useMediaQuery";
|
||||||
import { PullToRefresh } from "./PullToRefresh";
|
import { PullToRefresh } from "./PullToRefresh";
|
||||||
import { ChatConversationItem } from "./ChatConversationItem";
|
import { ChatConversationItem } from "./ChatConversationItem";
|
||||||
|
|
@ -21,10 +20,20 @@ import type { ChatConversationListItem } from "@paperclipai/shared";
|
||||||
|
|
||||||
interface ChatConversationListProps {
|
interface ChatConversationListProps {
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
/**
|
||||||
|
* Currently-selected conversation id. Phase 16a: owners pass this via
|
||||||
|
* props instead of reading from the legacy `ChatPanelContext`.
|
||||||
|
*/
|
||||||
|
activeConversationId: string | null;
|
||||||
|
onSelectConversation: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatConversationList({ companyId }: ChatConversationListProps) {
|
export function ChatConversationList({
|
||||||
const { activeConversationId, setActiveConversationId } = useChatPanel();
|
companyId,
|
||||||
|
activeConversationId,
|
||||||
|
onSelectConversation,
|
||||||
|
}: ChatConversationListProps) {
|
||||||
|
const setActiveConversationId = onSelectConversation;
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useRef, useEffect, useCallback, useState } from "react";
|
import { useRef, useEffect, useCallback, useState } from "react";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useChatMessages } from "../hooks/useChatMessages";
|
import { useChatMessages } from "../hooks/useChatMessages";
|
||||||
import { useChatPanel } from "../context/ChatPanelContext";
|
|
||||||
import { ChatMessage } from "./ChatMessage";
|
import { ChatMessage } from "./ChatMessage";
|
||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -21,6 +20,13 @@ interface ChatMessageListProps {
|
||||||
agentMap?: Map<string, { name: string; icon: string | null; role: AgentRole | null }>;
|
agentMap?: Map<string, { name: string; icon: string | null; role: AgentRole | null }>;
|
||||||
onBookmark?: (messageId: string) => void;
|
onBookmark?: (messageId: string) => void;
|
||||||
bookmarkedMessageIds?: Set<string>;
|
bookmarkedMessageIds?: Set<string>;
|
||||||
|
/**
|
||||||
|
* When set to a message id, the virtualizer scrolls that message into
|
||||||
|
* view and invokes `onScrollComplete` once. Phase 16a migrated these
|
||||||
|
* off the legacy `ChatPanelContext`; callers now own the state.
|
||||||
|
*/
|
||||||
|
scrollToMessageId?: string | null;
|
||||||
|
onScrollComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessageList({
|
export function ChatMessageList({
|
||||||
|
|
@ -36,9 +42,10 @@ export function ChatMessageList({
|
||||||
agentMap,
|
agentMap,
|
||||||
onBookmark,
|
onBookmark,
|
||||||
bookmarkedMessageIds,
|
bookmarkedMessageIds,
|
||||||
|
scrollToMessageId = null,
|
||||||
|
onScrollComplete,
|
||||||
}: ChatMessageListProps) {
|
}: ChatMessageListProps) {
|
||||||
const { messages, isLoading } = useChatMessages(conversationId);
|
const { messages, isLoading } = useChatMessages(conversationId);
|
||||||
const { scrollToMessageId, setScrollToMessageId } = useChatPanel();
|
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const [showJumpToBottom, setShowJumpToBottom] = useState(false);
|
const [showJumpToBottom, setShowJumpToBottom] = useState(false);
|
||||||
|
|
||||||
|
|
@ -84,13 +91,15 @@ export function ChatMessageList({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [streamingContent, isStreaming]);
|
}, [streamingContent, isStreaming]);
|
||||||
|
|
||||||
// Scroll to a specific message when scrollToMessageId is set
|
// Scroll to a specific message when scrollToMessageId is set. Phase 16a:
|
||||||
|
// the prop replaces the legacy `ChatPanelContext` getter; callers are
|
||||||
|
// responsible for clearing the id via `onScrollComplete`.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollToMessageId) return;
|
if (!scrollToMessageId) return;
|
||||||
const index = displayMessages.findIndex((m) => m.id === scrollToMessageId);
|
const index = displayMessages.findIndex((m) => m.id === scrollToMessageId);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
virtualizer.scrollToIndex(index, { align: "center" });
|
virtualizer.scrollToIndex(index, { align: "center" });
|
||||||
setScrollToMessageId(null);
|
onScrollComplete?.();
|
||||||
}
|
}
|
||||||
// TODO: if message not in current page (infinite scroll), scroll is best-effort only.
|
// TODO: if message not in current page (infinite scroll), scroll is best-effort only.
|
||||||
// Future iteration: load pages until message found, then scroll.
|
// Future iteration: load pages until message found, then scroll.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import { VoiceStep } from "./onboarding/VoiceStep";
|
||||||
import { TelegramStep } from "./onboarding/TelegramStep";
|
import { TelegramStep } from "./onboarding/TelegramStep";
|
||||||
import { useHardwareInfo } from "../hooks/useHardwareInfo";
|
import { useHardwareInfo } from "../hooks/useHardwareInfo";
|
||||||
import { updateNexusSettings, type NexusMode } from "../api/hardware";
|
import { updateNexusSettings, type NexusMode } from "../api/hardware";
|
||||||
import { useChatPanel } from "../context/ChatPanelContext";
|
|
||||||
import {
|
import {
|
||||||
Cpu,
|
Cpu,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
|
|
@ -82,7 +81,6 @@ export function OnboardingWizard() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
|
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
|
||||||
const [routeDismissed, setRouteDismissed] = useState(false);
|
const [routeDismissed, setRouteDismissed] = useState(false);
|
||||||
const { setChatOpen } = useChatPanel();
|
|
||||||
|
|
||||||
// Preserve wizard-show detection logic from the original OnboardingWizard
|
// Preserve wizard-show detection logic from the original OnboardingWizard
|
||||||
const routeOnboardingOptions =
|
const routeOnboardingOptions =
|
||||||
|
|
@ -308,12 +306,11 @@ export function OnboardingWizard() {
|
||||||
try {
|
try {
|
||||||
const company = await createWorkspace();
|
const company = await createWorkspace();
|
||||||
|
|
||||||
// [nexus] Mode-aware landing + chat panel open — same logic as
|
// [nexus] Mode-aware landing. Phase 16a removed the legacy chat drawer
|
||||||
// handleSubmit but with the chat drawer toggled on.
|
// toggle — the Assistant page is the chat surface itself now.
|
||||||
const landingPath = selectedMode === "project_builder" ? "dashboard" : "assistant";
|
const landingPath = selectedMode === "project_builder" ? "dashboard" : "assistant";
|
||||||
closeOnboarding();
|
closeOnboarding();
|
||||||
navigate(`/${company.issuePrefix}/${landingPath}`);
|
navigate(`/${company.issuePrefix}/${landingPath}`);
|
||||||
setChatOpen(true);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Setup failed. Please try again.");
|
setError(err instanceof Error ? err.message : "Setup failed. Please try again.");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { createRoot } from "react-dom/client";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
// Stub ChatConversationList to avoid pulling its dependency tree into the
|
// Stub ChatConversationList to avoid pulling its dependency tree into the
|
||||||
// test (react-query, ChatPanelContext, etc.). We only want to assert that
|
// test (react-query, etc.). We only want to assert that HistorySheet mounts
|
||||||
// HistorySheet mounts it when companyId is present.
|
// it when companyId is present.
|
||||||
vi.mock("../ChatConversationList", () => ({
|
vi.mock("../ChatConversationList", () => ({
|
||||||
ChatConversationList: ({ companyId }: { companyId: string }) => (
|
ChatConversationList: ({ companyId }: { companyId: string }) => (
|
||||||
<div data-testid="chat-conversation-list-stub" data-company-id={companyId}>
|
<div data-testid="chat-conversation-list-stub" data-company-id={companyId}>
|
||||||
|
|
@ -14,6 +14,9 @@ vi.mock("../ChatConversationList", () => ({
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Shared defaults for the Phase 16a prop-drilled chat selection state.
|
||||||
|
const NOOP_SELECT = () => {};
|
||||||
|
|
||||||
// jsdom does not ship window.matchMedia, so the Phase 15 `useMediaQuery`
|
// jsdom does not ship window.matchMedia, so the Phase 15 `useMediaQuery`
|
||||||
// hook inside HistorySheet would throw. Stub it with a controllable fake
|
// hook inside HistorySheet would throw. Stub it with a controllable fake
|
||||||
// so individual tests can toggle between the desktop slide-over and the
|
// so individual tests can toggle between the desktop slide-over and the
|
||||||
|
|
@ -71,33 +74,73 @@ describe("<HistorySheet />", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
it("renders nothing when closed", () => {
|
it("renders nothing when closed", () => {
|
||||||
render(<HistorySheet open={false} onClose={() => {}} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open={false}
|
||||||
|
onClose={() => {}}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(container.querySelector('[data-testid="history-sheet-panel"]')).toBeNull();
|
expect(container.querySelector('[data-testid="history-sheet-panel"]')).toBeNull();
|
||||||
expect(container.querySelector('[data-testid="history-sheet-backdrop"]')).toBeNull();
|
expect(container.querySelector('[data-testid="history-sheet-backdrop"]')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the panel and a backdrop when open", () => {
|
it("renders the panel and a backdrop when open", () => {
|
||||||
render(<HistorySheet open onClose={() => {}} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={() => {}}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(container.querySelector('[data-testid="history-sheet-panel"]')).not.toBeNull();
|
expect(container.querySelector('[data-testid="history-sheet-panel"]')).not.toBeNull();
|
||||||
expect(container.querySelector('[data-testid="history-sheet-backdrop"]')).not.toBeNull();
|
expect(container.querySelector('[data-testid="history-sheet-backdrop"]')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mounts ChatConversationList inside the panel when companyId is present", () => {
|
it("mounts ChatConversationList inside the panel when companyId is present", () => {
|
||||||
render(<HistorySheet open onClose={() => {}} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={() => {}}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const stub = container.querySelector('[data-testid="chat-conversation-list-stub"]');
|
const stub = container.querySelector('[data-testid="chat-conversation-list-stub"]');
|
||||||
expect(stub).not.toBeNull();
|
expect(stub).not.toBeNull();
|
||||||
expect(stub?.getAttribute("data-company-id")).toBe("co-1");
|
expect(stub?.getAttribute("data-company-id")).toBe("co-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a placeholder when companyId is null", () => {
|
it("shows a placeholder when companyId is null", () => {
|
||||||
render(<HistorySheet open onClose={() => {}} companyId={null} />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={() => {}}
|
||||||
|
companyId={null}
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(container.querySelector('[data-testid="chat-conversation-list-stub"]')).toBeNull();
|
expect(container.querySelector('[data-testid="chat-conversation-list-stub"]')).toBeNull();
|
||||||
expect(container.textContent ?? "").toContain("Select a workspace");
|
expect(container.textContent ?? "").toContain("Select a workspace");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onClose when the backdrop is clicked", () => {
|
it("calls onClose when the backdrop is clicked", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
render(<HistorySheet open onClose={onClose} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={onClose}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const backdrop = container.querySelector(
|
const backdrop = container.querySelector(
|
||||||
'[data-testid="history-sheet-backdrop"]',
|
'[data-testid="history-sheet-backdrop"]',
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
|
@ -109,7 +152,15 @@ describe("<HistorySheet />", () => {
|
||||||
|
|
||||||
it("calls onClose when ESC is pressed", () => {
|
it("calls onClose when ESC is pressed", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
render(<HistorySheet open onClose={onClose} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={onClose}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
act(() => {
|
act(() => {
|
||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
||||||
});
|
});
|
||||||
|
|
@ -118,7 +169,15 @@ describe("<HistorySheet />", () => {
|
||||||
|
|
||||||
it("does not listen for ESC when closed", () => {
|
it("does not listen for ESC when closed", () => {
|
||||||
const onClose = vi.fn();
|
const onClose = vi.fn();
|
||||||
render(<HistorySheet open={false} onClose={onClose} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open={false}
|
||||||
|
onClose={onClose}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
act(() => {
|
act(() => {
|
||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
||||||
});
|
});
|
||||||
|
|
@ -126,7 +185,15 @@ describe("<HistorySheet />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses a left-side panel positioned against the icon rail", () => {
|
it("uses a left-side panel positioned against the icon rail", () => {
|
||||||
render(<HistorySheet open onClose={() => {}} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={() => {}}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const panel = container.querySelector('[data-testid="history-sheet-panel"]') as HTMLElement;
|
const panel = container.querySelector('[data-testid="history-sheet-panel"]') as HTMLElement;
|
||||||
// The Tailwind class string should include left-[56px] and w-[320px].
|
// The Tailwind class string should include left-[56px] and w-[320px].
|
||||||
expect(panel.className).toContain("left-[56px]");
|
expect(panel.className).toContain("left-[56px]");
|
||||||
|
|
@ -137,7 +204,15 @@ describe("<HistorySheet />", () => {
|
||||||
it("renders a full-screen variant on mobile (< 768px)", () => {
|
it("renders a full-screen variant on mobile (< 768px)", () => {
|
||||||
mediaMatches = false;
|
mediaMatches = false;
|
||||||
installMatchMedia();
|
installMatchMedia();
|
||||||
render(<HistorySheet open onClose={() => {}} companyId="co-1" />);
|
render(
|
||||||
|
<HistorySheet
|
||||||
|
open
|
||||||
|
onClose={() => {}}
|
||||||
|
companyId="co-1"
|
||||||
|
activeConversationId={null}
|
||||||
|
onSelectConversation={NOOP_SELECT}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const panel = container.querySelector('[data-testid="history-sheet-panel"]') as HTMLElement;
|
const panel = container.querySelector('[data-testid="history-sheet-panel"]') as HTMLElement;
|
||||||
expect(panel.getAttribute("data-variant")).toBe("mobile");
|
expect(panel.getAttribute("data-variant")).toBe("mobile");
|
||||||
expect(panel.className).toContain("w-full");
|
expect(panel.className).toContain("w-full");
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,19 @@ export interface HistorySheetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
companyId: string | null;
|
companyId: string | null;
|
||||||
|
activeConversationId: string | null;
|
||||||
|
onSelectConversation: (id: string | null) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistorySheet({ open, onClose, companyId, className }: HistorySheetProps) {
|
export function HistorySheet({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
companyId,
|
||||||
|
activeConversationId,
|
||||||
|
onSelectConversation,
|
||||||
|
className,
|
||||||
|
}: HistorySheetProps) {
|
||||||
// ESC closes the sheet.
|
// ESC closes the sheet.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
@ -80,7 +89,11 @@ export function HistorySheet({ open, onClose, companyId, className }: HistoryShe
|
||||||
</header>
|
</header>
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
{companyId ? (
|
{companyId ? (
|
||||||
<ChatConversationList companyId={companyId} />
|
<ChatConversationList
|
||||||
|
companyId={companyId}
|
||||||
|
activeConversationId={activeConversationId}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="px-4 py-6 text-xs text-muted-foreground">
|
<p className="px-4 py-6 text-xs text-muted-foreground">
|
||||||
Select a workspace to view conversations.
|
Select a workspace to view conversations.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { ChatMessage, ChatConversationListResponse } from "@paperclipai/shared";
|
import type { ChatMessage, ChatConversationListResponse } from "@paperclipai/shared";
|
||||||
import { useNexusMode } from "../hooks/useNexusMode";
|
import { useNexusMode } from "../hooks/useNexusMode";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { useChatPanel } from "../context/ChatPanelContext";
|
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../context/ToastContext";
|
||||||
import { useVoice } from "../context/VoiceContext";
|
import { useVoice } from "../context/VoiceContext";
|
||||||
import { chatApi } from "../api/chat";
|
import { chatApi } from "../api/chat";
|
||||||
|
|
@ -42,7 +41,10 @@ export function PersonalAssistant() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { pushToast } = useToast();
|
const { pushToast } = useToast();
|
||||||
const { activeConversationId, setActiveConversationId } = useChatPanel();
|
// Phase 16a: `activeConversationId` is now local to the Assistant page.
|
||||||
|
// Previously this lived in the legacy `ChatPanelContext`; migrating it
|
||||||
|
// off unblocks deleting that context along with the old desktop drawer.
|
||||||
|
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
||||||
const voice = useVoice();
|
const voice = useVoice();
|
||||||
|
|
||||||
const companyId = selectedCompany?.id ?? null;
|
const companyId = selectedCompany?.id ?? null;
|
||||||
|
|
@ -368,6 +370,8 @@ export function PersonalAssistant() {
|
||||||
open={historyOpen}
|
open={historyOpen}
|
||||||
onClose={() => setHistoryOpen(false)}
|
onClose={() => setHistoryOpen(false)}
|
||||||
companyId={companyId}
|
companyId={companyId}
|
||||||
|
activeConversationId={selectedConvId}
|
||||||
|
onSelectConversation={setActiveConversationId}
|
||||||
/>
|
/>
|
||||||
<MemorySheet
|
<MemorySheet
|
||||||
open={memoryOpen}
|
open={memoryOpen}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue