Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions echo/frontend/src/features/sidebar/primitives/NavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -75,6 +76,7 @@ export const NavItem = ({
accent,
disabled,
inset,
state,
}: NavItemProps) => {
const localePath = useLocalePath(to);
const resolved = useResolvedPath(localePath);
Expand Down Expand Up @@ -110,6 +112,7 @@ export const NavItem = ({
return (
<NavLink
to={localePath}
state={state}
end={end}
className={`relative flex h-[30px] items-center gap-2 rounded-md text-sm leading-tight transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#4169e1] ${inset ? "pr-2 pl-8" : "px-2"}`}
style={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useWorkspace } from "@/hooks/useWorkspace";
import { isReadOnlyRole } from "@/lib/roles";
import { BackButton } from "../../primitives/BackButton";
import { NavItem } from "../../primitives/NavItem";
import { useSidebarView } from "../../hooks/useSidebarView";

export const ProjectHomeView = () => {
const { workspaceId, projectId } = useParams<{
Expand All @@ -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);
Expand Down Expand Up @@ -76,6 +78,11 @@ export const ProjectHomeView = () => {
{!isObserver && (
<NavItem
to={`${base}/chats/new`}
state={
params.conversationId
? { selectedConversationIds: [params.conversationId] }
: undefined
}
label={<Trans>Ask</Trans>}
icon={ChatCircleDotsIcon}
badge={chatsCountQuery.data || undefined}
Expand Down
15 changes: 10 additions & 5 deletions echo/frontend/src/routes/project/chat/NewChatRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand All @@ -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({
Expand Down Expand Up @@ -165,72 +172,101 @@ export const ProjectConversationRoute = () => {

{/* Header: name, title, tags. Created-on and duration live on the list page only. */}
<Stack gap="xs" {...testId("conversation-detail-header")}>
<Group gap="sm" align="center" wrap="wrap">
<Title order={1}>{primary}</Title>
{verified && (
<Tooltip label={t`Has verified artifacts`}>
<ThemeIcon
variant="subtle"
color="primary"
size={22}
aria-label={t`Verified artifacts`}
>
<IconRosetteDiscountCheck size={20} />
</ThemeIcon>
</Tooltip>
)}
{isAnonymized && (
<Tooltip label={t`Anonymized conversation`}>
<ThemeIcon
variant="subtle"
color="primary"
size={22}
aria-label={t`Anonymized conversation`}
>
<DetectiveIcon size={20} />
</ThemeIcon>
</Tooltip>
)}
{isLocked && (
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
<Stack gap="xs" className="min-w-0 flex-1">
<Group gap="sm" align="center" wrap="wrap">
<Title order={1}>{primary}</Title>
{verified && (
<Tooltip label={t`Has verified artifacts`}>
<ThemeIcon
variant="subtle"
color="primary"
size={22}
aria-label={t`Verified artifacts`}
>
<IconRosetteDiscountCheck size={20} />
</ThemeIcon>
</Tooltip>
)}
{isAnonymized && (
<Tooltip label={t`Anonymized conversation`}>
<ThemeIcon
variant="subtle"
color="primary"
size={22}
aria-label={t`Anonymized conversation`}
>
<DetectiveIcon size={20} />
</ThemeIcon>
</Tooltip>
)}
{isLocked && (
<Tooltip
label={t`Upgrade your workspace to view this conversation`}
>
<Badge
size="sm"
color="primary"
variant="light"
leftSection={<IconLock size={12} />}
>
<Trans>Locked</Trans>
</Badge>
</Tooltip>
)}
</Group>
{conversation?.title && conversation?.participant_name && (
<Text size="sm" c="dimmed">
{conversation.participant_name}
</Text>
)}
{tags.length > 0 && (
<Group gap={6} wrap="wrap">
{tags.map((tag) => {
const tagText = getTagText(tag);
if (!tagText) return null;
return (
<Badge
key={tag.id}
size="xs"
variant="light"
color="gray"
radius="sm"
classNames={{ label: "!text-graphite" }}
>
{tagText}
</Badge>
);
})}
</Group>
)}
</Stack>
{!isObserver && (
<Tooltip
label={t`Upgrade your workspace to view this conversation`}
disabled={!isLocked}
>
<Badge
size="sm"
color="primary"
<Button
variant="light"
leftSection={<IconLock size={12} />}
size="sm"
leftSection={<ChatCircleDotsIcon size={18} />}
disabled={isLocked}
onClick={() => {
if (!conversationId) return;
navigate(
`/w/${resolvedWorkspaceId}/projects/${projectId}/chats/new`,
{
state: { selectedConversationIds: [conversationId] },
},
);
}}
{...testId("conversation-ask-button")}
>
<Trans>Locked</Trans>
</Badge>
<Trans>Ask</Trans>
</Button>
</Tooltip>
)}
</Group>
{conversation?.title && conversation?.participant_name && (
<Text size="sm" c="dimmed">
{conversation.participant_name}
</Text>
)}
{tags.length > 0 && (
<Group gap={6} wrap="wrap">
{tags.map((tag) => {
const tagText = getTagText(tag);
if (!tagText) return null;
return (
<Badge
key={tag.id}
size="xs"
variant="light"
color="gray"
radius="sm"
classNames={{ label: "!text-graphite" }}
>
{tagText}
</Badge>
);
})}
</Group>
)}
</Stack>

{/*
Expand Down
Loading