From b2a42e0b549a993db843565e2d33c87e96497eff Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 10 Sep 2026 11:10:16 +0200 Subject: [PATCH] feat(chat): restore conversation-scoped ask from conversation view and sidebar --- .../features/sidebar/primitives/NavItem.tsx | 3 + .../sidebar/views/project/ProjectHomeView.tsx | 7 + .../src/routes/project/chat/NewChatRoute.tsx | 15 +- .../conversation/ProjectConversationRoute.tsx | 156 +++++++++++------- 4 files changed, 116 insertions(+), 65 deletions(-) diff --git a/echo/frontend/src/features/sidebar/primitives/NavItem.tsx b/echo/frontend/src/features/sidebar/primitives/NavItem.tsx index 151a0859a..b2975e7f3 100644 --- a/echo/frontend/src/features/sidebar/primitives/NavItem.tsx +++ b/echo/frontend/src/features/sidebar/primitives/NavItem.tsx @@ -22,6 +22,7 @@ interface NavItemProps { disabled?: boolean; /** Indent to align with an icon-bearing row's label, for sub-rows. */ inset?: boolean; + state?: unknown; } export const BADGE_TONES = { @@ -75,6 +76,7 @@ export const NavItem = ({ accent, disabled, inset, + state, }: NavItemProps) => { const localePath = useLocalePath(to); const resolved = useResolvedPath(localePath); @@ -110,6 +112,7 @@ export const NavItem = ({ return ( { const { workspaceId, projectId } = useParams<{ @@ -30,6 +31,7 @@ export const ProjectHomeView = () => { }>(); const { pathname } = useLocation(); const { workspace } = useWorkspace(); + const { params } = useSidebarView(); // Observers are read-only and have no chat access. Hide the Ask tab and skip // its count query (it 403s for them); passing "" disables the query. const isObserver = isReadOnlyRole(workspace?.role); @@ -76,6 +78,11 @@ export const ProjectHomeView = () => { {!isObserver && ( Ask} icon={ChatCircleDotsIcon} badge={chatsCountQuery.data || undefined} diff --git a/echo/frontend/src/routes/project/chat/NewChatRoute.tsx b/echo/frontend/src/routes/project/chat/NewChatRoute.tsx index c8251deb1..ef4535f57 100644 --- a/echo/frontend/src/routes/project/chat/NewChatRoute.tsx +++ b/echo/frontend/src/routes/project/chat/NewChatRoute.tsx @@ -293,11 +293,16 @@ export const NewChatRoute = () => { const state = location.state as { selectedConversationIds?: unknown; } | null; - return Array.isArray(state?.selectedConversationIds) - ? state.selectedConversationIds.filter( - (id): id is string => typeof id === "string", - ) - : []; + if (Array.isArray(state?.selectedConversationIds)) { + return state.selectedConversationIds.filter( + (id): id is string => typeof id === "string", + ); + } + const params = new URLSearchParams(location.search); + const paramId = + params.get("conversationId") ?? params.get("conversation_id"); + if (paramId) return [paramId]; + return []; }); const [pickerOpened, pickerHandlers] = useDisclosure(false); const [agenticIntroOpened, agenticIntroHandlers] = useDisclosure(false); diff --git a/echo/frontend/src/routes/project/conversation/ProjectConversationRoute.tsx b/echo/frontend/src/routes/project/conversation/ProjectConversationRoute.tsx index bc7032abd..9cfbec9ec 100644 --- a/echo/frontend/src/routes/project/conversation/ProjectConversationRoute.tsx +++ b/echo/frontend/src/routes/project/conversation/ProjectConversationRoute.tsx @@ -15,7 +15,7 @@ import { Tooltip, } from "@mantine/core"; import { useClipboard, useDisclosure } from "@mantine/hooks"; -import { DetectiveIcon } from "@phosphor-icons/react"; +import { ChatCircleDotsIcon, DetectiveIcon } from "@phosphor-icons/react"; import { IconLock, IconRefresh, @@ -47,7 +47,10 @@ import { ENABLE_DISPLAY_CONVERSATION_LINKS, TRANSCRIPT_TROUBLESHOOTING_DOCS_URL, } from "@/config"; +import { useI18nNavigate } from "@/hooks/useI18nNavigate"; +import { useWorkspace } from "@/hooks/useWorkspace"; import { generateConversationSummary } from "@/lib/api"; +import { isReadOnlyRole } from "@/lib/roles"; import { testId } from "@/lib/testUtils"; const getTagText = (tag: ConversationProjectTag) => { @@ -61,7 +64,11 @@ const hasVerifiedArtifacts = (conversation: Conversation) => ) ?? false; export const ProjectConversationRoute = () => { - const { conversationId, projectId } = useParams(); + const { conversationId, projectId, workspaceId } = useParams(); + const navigate = useI18nNavigate(); + const { workspace } = useWorkspace(); + const isObserver = isReadOnlyRole(workspace?.role); + const resolvedWorkspaceId = workspaceId ?? workspace?.id; const queryClient = useQueryClient(); const conversationQuery = useConversationById({ @@ -165,72 +172,101 @@ export const ProjectConversationRoute = () => { {/* Header: name, title, tags. Created-on and duration live on the list page only. */} - - {primary} - {verified && ( - - - - - - )} - {isAnonymized && ( - - - - - - )} - {isLocked && ( + + + + {primary} + {verified && ( + + + + + + )} + {isAnonymized && ( + + + + + + )} + {isLocked && ( + + } + > + Locked + + + )} + + {conversation?.title && conversation?.participant_name && ( + + {conversation.participant_name} + + )} + {tags.length > 0 && ( + + {tags.map((tag) => { + const tagText = getTagText(tag); + if (!tagText) return null; + return ( + + {tagText} + + ); + })} + + )} + + {!isObserver && ( - } + size="sm" + leftSection={} + disabled={isLocked} + onClick={() => { + if (!conversationId) return; + navigate( + `/w/${resolvedWorkspaceId}/projects/${projectId}/chats/new`, + { + state: { selectedConversationIds: [conversationId] }, + }, + ); + }} + {...testId("conversation-ask-button")} > - Locked - + Ask + )} - {conversation?.title && conversation?.participant_name && ( - - {conversation.participant_name} - - )} - {tags.length > 0 && ( - - {tags.map((tag) => { - const tagText = getTagText(tag); - if (!tagText) return null; - return ( - - {tagText} - - ); - })} - - )} {/*