mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
198 lines
9.9 KiB
TypeScript
198 lines
9.9 KiB
TypeScript
import { useActions, useValues } from 'kea'
|
|
import { useEffect } from 'react'
|
|
|
|
import { IconShare, IconX } from '@hanzo/icons'
|
|
import { Button, Modal } from '@hanzo/elements'
|
|
|
|
import { copyToClipboard } from 'lib/utils/copyToClipboard'
|
|
import { colonDelimitedDuration } from 'lib/utils/durations'
|
|
import { playerMetaLogic } from 'scenes/session-recordings/player/player-meta/playerMetaLogic'
|
|
import { sessionSummaryProgressLogic } from 'scenes/session-recordings/player/player-meta/sessionSummaryProgressLogic'
|
|
import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer'
|
|
import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'
|
|
|
|
import { SessionGroupSummaryDetailsMetadata } from './SessionGroupSummaryDetailsMetadata'
|
|
import { PatternAssignedEventSegmentContext } from './types'
|
|
import { getIssueTags } from './utils'
|
|
|
|
interface SessionDetailsModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
event: PatternAssignedEventSegmentContext | null
|
|
}
|
|
|
|
export function SessionGroupSummaryDetailsModal({ isOpen, onClose, event }: SessionDetailsModalProps): JSX.Element {
|
|
const playerKey = 'session-details-modal'
|
|
const sessionRecordingId = event?.target_event?.session_id
|
|
const logicProps = {
|
|
sessionRecordingId: sessionRecordingId || '',
|
|
playerKey,
|
|
autoPlay: false,
|
|
}
|
|
const { seekToTime } = useActions(sessionRecordingPlayerLogic(logicProps))
|
|
const { sessionPlayerData } = useValues(sessionRecordingPlayerLogic(logicProps))
|
|
const { setSummaryOpen } = useActions(sessionSummaryProgressLogic)
|
|
const { summarizeSession } = useActions(playerMetaLogic(logicProps))
|
|
const { sessionSummary } = useValues(playerMetaLogic(logicProps))
|
|
// Scrolling to a bit before the moment to better notice it
|
|
const timeToSeekTo = (ms: number): number => Math.max(ms - 4000, 0)
|
|
// Seek to target event timestamp when modal opens and player is loaded
|
|
useEffect(() => {
|
|
if (isOpen && event?.target_event && sessionPlayerData) {
|
|
seekToTime(timeToSeekTo(event.target_event.milliseconds_since_start))
|
|
}
|
|
}, [isOpen, event, sessionPlayerData, seekToTime])
|
|
useEffect(() => {
|
|
if (isOpen && sessionPlayerData && sessionRecordingId) {
|
|
setSummaryOpen(sessionRecordingId, true)
|
|
if (!sessionSummary) {
|
|
summarizeSession()
|
|
}
|
|
}
|
|
}, [isOpen, sessionPlayerData, sessionSummary, sessionRecordingId, setSummaryOpen, summarizeSession])
|
|
// Handle conditional rendering after all hooks
|
|
if (!event || !event.target_event) {
|
|
return <></>
|
|
}
|
|
// Raise error if recording id is not found
|
|
if (!sessionRecordingId) {
|
|
throw new Error('Session recording id not found')
|
|
}
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
simple
|
|
title=""
|
|
width={1600}
|
|
closable={true}
|
|
hideCloseButton={true}
|
|
>
|
|
<Modal.Content embedded>
|
|
<div className="flex flex-col">
|
|
{/* Header */}
|
|
<header className="flex items-center justify-between p-4 border-b">
|
|
<div>
|
|
<h2 className="text-lg font-semibold mb-1">
|
|
{event.target_event.description}
|
|
<span className="text-muted font-normal ml-2">
|
|
@ {colonDelimitedDuration(event.target_event.milliseconds_since_start / 1000)}
|
|
</span>
|
|
</h2>
|
|
<SessionGroupSummaryDetailsMetadata
|
|
event={event}
|
|
issueTags={getIssueTags(event.target_event)}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{/* TODO: Enable thumbs up/down for feedback */}
|
|
{/* <Button size="small" icon={<IconThumbsUp />} />
|
|
<Button size="small" icon={<IconThumbsDown />} />
|
|
<div className="h-6 w-px bg-border mx-2" /> */}
|
|
<Button
|
|
size="small"
|
|
icon={<IconShare />}
|
|
onClick={() => void copyToClipboard(window.location.href, 'link')}
|
|
/>
|
|
<Button size="small" icon={<IconX />} onClick={onClose} />
|
|
</div>
|
|
</header>
|
|
|
|
{/* Content Grid */}
|
|
<div className="grid grid-cols-3 gap-8 p-6">
|
|
{/* Quick Summary Section */}
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-muted mb-1">What user was doing</h4>
|
|
<p className="text-sm mb-0">{event.segment_name}</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-medium text-muted mb-1">What's the outcome</h4>
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<b>{event.segment_success ? 'Successful' : 'Failed'}.</b>{' '}
|
|
{event.segment_outcome}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-muted mb-1">What confirmed the pattern</h4>
|
|
<p className="text-sm mb-0">
|
|
{event.target_event.description}
|
|
{event.target_event.event && (
|
|
<>
|
|
{' ('}
|
|
<code className="text-xs text-muted bg-fill-secondary px-1 py-0.5 rounded">
|
|
{event.target_event.event}
|
|
{event.target_event.event_type &&
|
|
` (${event.target_event.event_type})`}
|
|
</code>
|
|
)
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-medium text-muted mb-1">Where it happened</h4>
|
|
<p
|
|
className="text-sm mb-0 break-all truncate"
|
|
title={event.target_event.current_url || undefined}
|
|
>
|
|
{event.target_event.current_url || 'unknown'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Outcome Section */}
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-muted mb-1">What happened before</h4>
|
|
{event.previous_events_in_segment.length > 0 ? (
|
|
<ul className="text-sm mb-0 list-disc list-inside space-y-1">
|
|
{event.previous_events_in_segment.map((e, idx) => (
|
|
<li key={idx}>{e.description}</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="text-sm mb-0">Nothing happened</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-medium text-muted mb-1">What happened after</h4>
|
|
{event.next_events_in_segment.length > 0 ? (
|
|
<ul className="text-sm mb-0 list-disc list-inside space-y-1">
|
|
{event.next_events_in_segment.map((e, idx) => (
|
|
<li key={idx}>{e.description}</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="text-sm mb-0">Nothing happened</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Replay Player */}
|
|
<div className="w-full">
|
|
<SessionRecordingPlayer
|
|
sessionRecordingId={sessionRecordingId}
|
|
playerKey={playerKey}
|
|
autoPlay={false}
|
|
noBorder={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Modal.Content>
|
|
</Modal>
|
|
)
|
|
}
|