diff --git a/apps/code/src/renderer/assets/images/explorer-hog.png b/apps/code/src/renderer/assets/images/explorer-hog.png new file mode 100644 index 000000000..95df75d4c Binary files /dev/null and b/apps/code/src/renderer/assets/images/explorer-hog.png differ diff --git a/apps/code/src/renderer/assets/images/graphs-hog.png b/apps/code/src/renderer/assets/images/graphs-hog.png new file mode 100644 index 000000000..fa0b1e9c7 Binary files /dev/null and b/apps/code/src/renderer/assets/images/graphs-hog.png differ diff --git a/apps/code/src/renderer/assets/images/mail-hog.png b/apps/code/src/renderer/assets/images/mail-hog.png new file mode 100644 index 000000000..9f95210b0 Binary files /dev/null and b/apps/code/src/renderer/assets/images/mail-hog.png differ diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 098edeecc..432b9637c 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -1,4 +1,3 @@ -import { ResizableSidebar } from "@components/ResizableSidebar"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail"; import { @@ -13,21 +12,24 @@ import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignals import { buildSignalTaskPrompt } from "@features/inbox/utils/buildSignalTaskPrompt"; import { buildSignalReportListOrdering, + buildStatusFilterParam, filterReportsBySearch, } from "@features/inbox/utils/filterReports"; -import { - INBOX_PIPELINE_STATUS_FILTER, - INBOX_REFETCH_INTERVAL_MS, -} from "@features/inbox/utils/inboxConstants"; +import { INBOX_REFETCH_INTERVAL_MS } from "@features/inbox/utils/inboxConstants"; import { useDraftStore } from "@features/message-editor/stores/draftStore"; -import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; +import { SignalSourcesSettings } from "@features/settings/components/sections/SignalSourcesSettings"; import { useCreateTask } from "@features/tasks/hooks/useTasks"; import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useRepositoryIntegration } from "@hooks/useIntegrations"; import { + ArrowDownIcon, ArrowSquareOutIcon, + ArrowsClockwiseIcon, + CircleNotchIcon, ClockIcon, Cloud as CloudIcon, + GithubLogoIcon, + WarningIcon, XIcon, } from "@phosphor-icons/react"; import { @@ -35,23 +37,27 @@ import { Badge, Box, Button, + Dialog, Flex, ScrollArea, Select, Text, + Tooltip, } from "@radix-ui/themes"; +import explorerHog from "@renderer/assets/images/explorer-hog.png"; +import graphsHog from "@renderer/assets/images/graphs-hog.png"; +import mailHog from "@renderer/assets/images/mail-hog.png"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import type { SignalReportArtefact, SignalReportArtefactsResponse, SignalReportsQueryParams, + SuggestedReviewersArtefact, } from "@shared/types"; import { useNavigationStore } from "@stores/navigationStore"; import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { SignalsErrorState, SignalsLoadingState } from "./InboxEmptyStates"; -import { InboxWarmingUpState } from "./InboxWarmingUpState"; import { ReportCard } from "./ReportCard"; import { SignalCard } from "./SignalCard"; import { SignalReportPriorityBadge } from "./SignalReportPriorityBadge"; @@ -115,13 +121,187 @@ function LoadMoreTrigger({ ); } +// ── Animated ellipsis for warming-up inline text ───────────────────────────── + +function AnimatedEllipsis() { + return ( + + + . + . + . + + + ); +} + +// ── Right pane empty states ───────────────────────────────────────────────── + +function WelcomePane({ onEnableInbox }: { onEnableInbox: () => void }) { + return ( + + + + + + Welcome to your Inbox + + + + + + Background analysis of your data — while you sleep. + +
+ Session recordings watched automatically. Issues, tickets, and evals + analyzed around the clock. +
+ + + + + + Ready-to-run fixes for real user problems. + +
+ Each report includes evidence and impact numbers — just execute the + prompt in your agent. +
+
+ + +
+
+ ); +} + +function WarmingUpPane({ + onConfigureSources, +}: { + onConfigureSources: () => void; +}) { + return ( + + + + + + Inbox is warming up + + + + + Reports will appear here as soon as signals come in. + + + + + + ); +} + +function SelectReportPane() { + return ( + + + + + Select a report + + + Pick a report from the list to see details, signals, and evidence. + + + + ); +} + +// ── Main component ────────────────────────────────────────────────────────── + export function InboxSignalsTab() { const sortField = useInboxSignalsFilterStore((s) => s.sortField); const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection); const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery); + const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter); const { data: signalSourceConfigs } = useSignalSourceConfigs(); const hasSignalSources = signalSourceConfigs?.some((c) => c.enabled) ?? false; - const openSettings = useSettingsDialogStore((s) => s.open); + const [sourcesDialogOpen, setSourcesDialogOpen] = useState(false); const windowFocused = useRendererWindowFocusStore((s) => s.focused); const isInboxView = useNavigationStore((s) => s.view.type === "inbox"); @@ -129,10 +309,10 @@ export function InboxSignalsTab() { const inboxQueryParams = useMemo( (): SignalReportsQueryParams => ({ - status: INBOX_PIPELINE_STATUS_FILTER, + status: buildStatusFilterParam(statusFilter), ordering: buildSignalReportListOrdering(sortField, sortDirection), }), - [sortField, sortDirection], + [statusFilter, sortField, sortDirection], ); const { @@ -164,12 +344,10 @@ export function InboxSignalsTab() { [allReports], ); const [selectedReportId, setSelectedReportId] = useState(null); - const sidebarOpen = useInboxSignalsSidebarStore((state) => state.open); const sidebarWidth = useInboxSignalsSidebarStore((state) => state.width); const sidebarIsResizing = useInboxSignalsSidebarStore( (state) => state.isResizing, ); - const setSidebarOpen = useInboxSignalsSidebarStore((state) => state.setOpen); const setSidebarWidth = useInboxSignalsSidebarStore( (state) => state.setWidth, ); @@ -190,9 +368,8 @@ export function InboxSignalsTab() { ); if (!selectedExists) { setSelectedReportId(null); - setSidebarOpen(false); } - }, [reports, selectedReportId, setSidebarOpen]); + }, [reports, selectedReportId]); const selectedReport = useMemo( () => reports.find((report) => report.id === selectedReportId) ?? null, @@ -202,9 +379,16 @@ export function InboxSignalsTab() { const artefactsQuery = useInboxReportArtefacts(selectedReport?.id ?? "", { enabled: !!selectedReport, }); - const visibleArtefacts = (artefactsQuery.data?.results ?? []).filter( - (a): a is SignalReportArtefact => a.type === "video_segment", + const allArtefacts = artefactsQuery.data?.results ?? []; + const visibleArtefacts = allArtefacts.filter( + (a): a is SignalReportArtefact => a.type !== "suggested_reviewers", ); + const suggestedReviewers = useMemo(() => { + const reviewerArtefact = allArtefacts.find( + (a): a is SuggestedReviewersArtefact => a.type === "suggested_reviewers", + ); + return reviewerArtefact?.content ?? []; + }, [allArtefacts]); const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason; const showArtefactsUnavailable = !artefactsQuery.isLoading && @@ -299,302 +483,507 @@ export function InboxSignalsTab() { githubIntegration?.id, ]); - if (isLoading) { - return ; - } + // Resize handle for left pane + const containerRef = useRef(null); - if (error) { - return ( - { - void refetch(); - }} - isRetrying={isFetching} - /> - ); - } + const handleResizeMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setSidebarIsResizing(true); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [setSidebarIsResizing], + ); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!sidebarIsResizing || !containerRef.current) return; + const containerLeft = containerRef.current.getBoundingClientRect().left; + const containerWidth = containerRef.current.offsetWidth; + const maxWidth = containerWidth * 0.6; + const newWidth = Math.max( + 220, + Math.min(maxWidth, e.clientX - containerLeft), + ); + setSidebarWidth(newWidth); + }; + const handleMouseUp = () => { + if (sidebarIsResizing) { + setSidebarIsResizing(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + }; + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [sidebarIsResizing, setSidebarWidth, setSidebarIsResizing]); + + // ── Layout mode: full-width empty state vs two-pane ───────────────────── - if (allReports.length === 0) { - if (!hasSignalSources) { - return ( + const hasReports = allReports.length > 0; + const showTwoPaneLayout = hasReports || !!searchQuery.trim(); + + // ── Determine right pane content (only used in two-pane mode) ────────── + + let rightPaneContent: React.ReactNode; + + if (selectedReport) { + rightPaneContent = ( + <> - + - Enable Inbox - - - Inbox automatically analyzes your product data and prioritizes - actionable tasks. Choose which sources to enable for this project. + {selectedReport.title ?? "Untitled signal"} + - + + + {cloudModeEnabled && ( + + )} + + {!canActOnReport && selectedReport ? ( + + {selectedReport.status === "pending_input" + ? "This report needs input in PostHog before an agent can act on it." + : "Research is still running — you can read context below, then create a task when status is Ready."} + + ) : null} - ); - } - return ; - } - - return ( - - - - - - {reports.length === 0 && searchQuery.trim() ? ( - - - No matching signals - - - ) : null} - {reports.map((report, index) => ( - { - setSelectedReportId(report.id); - setSidebarOpen(true); - }} - /> - ))} - + - - - - - - {selectedReport ? ( - <> - - + + + + {selectedReport.signal_count} occurrences + + + {selectedReport.relevant_user_count ?? 0} affected users + + + + {suggestedReviewers.length > 0 && ( + - {selectedReport.title ?? "Untitled signal"} + Suggested reviewers - - - - - {cloudModeEnabled && ( - - )} - - {!canActOnReport && selectedReport ? ( + + {suggestedReviewers.map((reviewer) => ( + + + + {reviewer.user?.first_name ?? + reviewer.github_name ?? + reviewer.github_login} + + + @{reviewer.github_login} + + + ))} + + + )} + + {signals.length > 0 && ( + - {selectedReport.status === "pending_input" - ? "This report needs input in PostHog before an agent can act on it." - : "Research is still running — you can read context below, then create a task when status is Ready."} + Signals ({signals.length}) - ) : null} - - - - - - - - {selectedReport.signal_count} occurrences - - - {selectedReport.relevant_user_count ?? 0} affected users - + + {signals.map((signal) => ( + + ))} + + )} + {signalsQuery.isLoading && ( + + Loading signals... + + )} + + + + Evidence + + {artefactsQuery.isLoading && ( + + Loading evidence... + + )} + {showArtefactsUnavailable && ( + + {artefactsUnavailableMessage} + + )} + {!artefactsQuery.isLoading && + !showArtefactsUnavailable && + visibleArtefacts.length === 0 && ( + + No artefacts were returned for this signal. + + )} - {signals.length > 0 && ( - + + {visibleArtefacts.map((artefact) => ( + - Signals ({signals.length}) + {artefact.content.content} - - {signals.map((signal) => ( - - ))} + + + + + {artefact.content.start_time + ? new Date( + artefact.content.start_time, + ).toLocaleString() + : "Unknown time"} + + + {replayBaseUrl && artefact.content.session_id && ( + + View replay + + + )} - )} - {signalsQuery.isLoading && ( - - Loading signals... - - )} + ))} + + + + + + ); + } else { + rightPaneContent = ; + } - - - Evidence - - {artefactsQuery.isLoading && ( - - Loading evidence... - - )} - {showArtefactsUnavailable && ( - - {artefactsUnavailableMessage} - - )} - {!artefactsQuery.isLoading && - !showArtefactsUnavailable && - visibleArtefacts.length === 0 && ( - - No session segments available for this report. - - )} + // ── Left pane content ─────────────────────────────────────────────────── - - {visibleArtefacts.map((artefact) => ( - - - {artefact.content.content} - - - - - - {artefact.content.start_time - ? new Date( - artefact.content.start_time, - ).toLocaleString() - : "Unknown time"} - - - {replayBaseUrl && artefact.content.session_id && ( - - View replay - - - )} - - - ))} - - + let leftPaneList: React.ReactNode; + + if (isLoading && allReports.length === 0 && hasSignalSources) { + leftPaneList = ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + + ))} + + ); + } else if (error) { + leftPaneList = ( + + + + + Could not load signals + + + + + ); + } else if (reports.length === 0 && searchQuery.trim()) { + leftPaneList = ( + + + No matching signals + + + ); + } else { + leftPaneList = ( + <> + {reports.map((report, index) => ( + setSelectedReportId(report.id)} + /> + ))} + + + ); + } + + // ── Skeleton rows for backdrop behind empty states ────────────────────── + + const skeletonBackdrop = ( + + {Array.from({ length: 8 }).map((_, index) => ( + + + + + ))} + + ); + + const searchDisabledReason = + !hasReports && !searchQuery.trim() + ? "No reports in the project\u2026 yet" + : null; + + return ( + <> + {showTwoPaneLayout ? ( + + {/* ── Left pane: report list ───────────────────────────────── */} + + + + + + {leftPaneList} - - ) : null} - + {/* Resize handle */} + + + + {/* ── Right pane: detail ───────────────────────────────────── */} + + {rightPaneContent} + + + ) : ( + /* ── Full-width empty state with skeleton backdrop ──────────── */ + + + + {skeletonBackdrop} + + + {!hasSignalSources ? ( + setSourcesDialogOpen(true)} /> + ) : ( + setSourcesDialogOpen(true)} + /> + )} + + + )} + + {/* ── Sources config dialog ──────────────────────────────────── */} + + + + + Signal sources + + + + + + + + {hasSignalSources ? ( + + + + ) : ( + + + + )} + + + + + {/* ── Cloud task confirmation dialog ────────────────────────── */} { @@ -663,6 +1052,6 @@ export function InboxSignalsTab() { - + ); } diff --git a/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx b/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx index 427ad723d..c2ef1bdba 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx @@ -1,33 +1,32 @@ import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceConfigs"; -import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; import { - BrainIcon, + BugIcon, GithubLogoIcon, KanbanIcon, SparkleIcon, TicketIcon, VideoIcon, } from "@phosphor-icons/react"; -import { Flex, Text, Tooltip } from "@radix-ui/themes"; +import { Button, Flex, Text, Tooltip } from "@radix-ui/themes"; import type { SignalSourceConfig } from "@renderer/api/posthogClient"; -import { motion } from "framer-motion"; +import explorerHog from "@renderer/assets/images/explorer-hog.png"; import { type ReactNode, useMemo } from "react"; const SOURCE_DISPLAY_ORDER: SignalSourceConfig["source_product"][] = [ "session_replay", - "llm_analytics", + "error_tracking", "github", "linear", "zendesk", ]; function sourceIcon(product: SignalSourceConfig["source_product"]): ReactNode { - const common = { size: 22 as const }; + const common = { size: 20 as const }; switch (product) { case "session_replay": return ; - case "llm_analytics": - return ; + case "error_tracking": + return ; case "github": return ; case "linear": @@ -39,6 +38,25 @@ function sourceIcon(product: SignalSourceConfig["source_product"]): ReactNode { } } +function sourceProductTooltipLabel( + product: SignalSourceConfig["source_product"], +): string { + switch (product) { + case "session_replay": + return "PostHog Session Replay"; + case "error_tracking": + return "PostHog Error Tracking"; + case "github": + return "GitHub Issues"; + case "linear": + return "Linear"; + case "zendesk": + return "Zendesk"; + default: + return "Signal source"; + } +} + function AnimatedEllipsis({ className }: { className?: string }) { return ( @@ -51,17 +69,29 @@ function AnimatedEllipsis({ className }: { className?: string }) { ); } -export function InboxWarmingUpState() { +interface InboxWarmingUpStateProps { + onConfigureSources: () => void; +} + +export function InboxWarmingUpState({ + onConfigureSources, +}: InboxWarmingUpStateProps) { const { data: configs } = useSignalSourceConfigs(); - const openSignalSettings = useSettingsDialogStore((s) => s.open); - const enabledSources = useMemo(() => { - const enabled = (configs ?? []).filter((c) => c.enabled); - return [...enabled].sort( - (a, b) => - SOURCE_DISPLAY_ORDER.indexOf(a.source_product) - - SOURCE_DISPLAY_ORDER.indexOf(b.source_product), - ); + const enabledProducts = useMemo(() => { + const seen = new Set(); + return (configs ?? []) + .filter((c) => c.enabled) + .sort( + (a, b) => + SOURCE_DISPLAY_ORDER.indexOf(a.source_product) - + SOURCE_DISPLAY_ORDER.indexOf(b.source_product), + ) + .filter((c) => { + if (seen.has(c.source_product)) return false; + seen.add(c.source_product); + return true; + }); }, [configs]); return ( @@ -69,88 +99,59 @@ export function InboxWarmingUpState() { direction="column" align="center" justify="center" - gap="4" height="100%" - px="4" - className="text-center" + px="5" + style={{ margin: "0 auto" }} > - - - + + - - + Inbox is warming up + - - Reports appear here as soon as signals are grouped. Research usually - finishes within a minute while we watch your connected sources. - - - Processing signals - - - - {enabledSources.length > 0 ? ( - - + + + {enabledProducts.map((cfg) => ( + + + {sourceIcon(cfg.source_product)} + + + ))} + - - - ) : null} + Configure sources + + + ); }