feat(chrome-extension): enhance recording UI and session management

- Simplified the Record component structure by separating list and detail views for better user experience.
- Improved styling for the recording sessions list and detail views, ensuring better responsiveness and usability.
- Added functionality to switch between list and detail views for recording sessions.
- Updated session management to allow for viewing, editing, and deleting sessions with improved user feedback.
- Enhanced event handling and display for recorded events within the detail view.
This commit is contained in:
zhouxiao.shaw 2025-05-29 08:32:29 +08:00
parent cd293fcf53
commit 0ee39bd9a6
3 changed files with 850 additions and 318 deletions

View File

@ -52,9 +52,7 @@ export function PlaygroundPopup() {
label: 'Record',
icon: <VideoCameraOutlined />,
children: (
<div className="popup-record-container">
<Record />
</div>
<Record />
),
},
{

View File

@ -2,36 +2,322 @@
min-height: 500px;
max-height: 600px;
overflow-y: auto;
overflow-x: hidden; // 禁用横向滚动条
padding: 16px;
box-sizing: border-box;
width: 100%;
// 统一滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a1a1a1;
}
}
// 防止所有子元素超出容器宽度
* {
max-width: 100%;
box-sizing: border-box;
}
// View-specific styles
.record-list-view {
width: 100%;
max-width: 100%;
.session-list {
width: 100%;
max-width: 100%;
.ant-list-item {
padding: 8px 0;
width: 100%;
max-width: 100%;
.ant-card {
transition: all 0.2s ease;
width: 100%;
max-width: 100%;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
&.selected-session {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
.ant-card-head {
background-color: #f0f7ff;
}
}
.ant-card-actions {
background-color: #fafafa;
.ant-btn {
border: none;
box-shadow: none;
&:hover {
background-color: #e6f7ff;
}
&.ant-btn-dangerous:hover {
background-color: #fff2f0;
}
}
}
.session-meta {
width: 100%;
max-width: 100%;
.session-status {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.session-details {
font-size: 12px;
color: #999;
line-height: 1.4;
word-break: break-all;
white-space: normal;
}
}
}
}
}
.session-empty {
text-align: center;
padding: 40px 20px;
width: 100%;
max-width: 100%;
.ant-empty-description {
color: #999;
}
}
}
.record-detail-view {
width: 100%;
max-width: 100%;
// Header section
.detail-header {
width: 100%;
max-width: 100%;
display: flex;
align-items: flex-start;
margin-bottom: 16px;
.back-button {
margin-right: 12px;
flex-shrink: 0;
margin-top: 4px; // 对齐标题
}
.session-title {
flex: 1;
min-width: 0; // 允许文本截断
max-width: calc(100% - 120px); // 为返回按钮留出空间
h4 {
margin: 0;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.ant-typography {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
width: 100%;
}
}
}
// Recording status section
.recording-status {
width: 100%;
max-width: 100%;
margin-bottom: 16px;
word-wrap: break-word;
}
// Session info card
.session-info-card {
width: 100%;
max-width: 100%;
margin-bottom: 16px;
.ant-card-body {
padding: 16px;
}
}
// Controls section
.controls-section {
width: 100%;
max-width: 100%;
margin-bottom: 16px;
.current-tab-info {
width: 100%;
max-width: 100%;
margin-bottom: 12px;
word-break: break-all;
white-space: normal;
}
.record-controls {
width: 100%;
max-width: 100%;
display: flex;
gap: 8px;
flex-wrap: wrap;
.ant-btn {
flex-shrink: 0;
max-width: 100%;
}
}
}
// Events section
.events-section {
width: 100%;
max-width: 100%;
.events-header {
width: 100%;
max-width: 100%;
margin-bottom: 12px;
h5 {
margin: 0;
font-weight: 600;
}
}
.events-container {
width: 100%;
max-width: 100%;
&.empty {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-style: italic;
min-height: 100px;
}
}
}
.session-info {
.ant-space {
width: 100%;
max-width: 100%;
> div {
display: flex;
align-items: flex-start;
width: 100%;
max-width: 100%;
.ant-typography {
margin: 0;
word-break: break-all;
white-space: normal;
max-width: 100%;
}
}
}
}
}
.record-timeline {
max-height: 400px;
overflow-y: auto;
padding: 8px;
// 移除内部滚动条和固定高度,让内容自然展开
width: 100%;
max-width: 100%;
padding: 16px;
border: 1px solid #f0f0f0;
border-radius: 6px;
background-color: #fafafa;
overflow: visible;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
// 确保内部组件也不产生滚动条并充分利用宽度
.ant-timeline {
width: 100%;
max-width: 100%;
overflow: visible;
&:hover {
background: #a1a1a1;
.ant-timeline-item {
width: 100%;
max-width: 100%;
.ant-timeline-item-content {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
}
}
.ant-card {
width: 100%;
max-width: 100%;
overflow: visible;
box-sizing: border-box;
.ant-card-body {
width: 100%;
max-width: 100%;
box-sizing: border-box;
padding: 12px 16px;
word-wrap: break-word;
}
}
.ant-space {
width: 100%;
max-width: 100%;
.ant-space-item {
width: 100%;
max-width: 100%;
word-wrap: break-word;
}
}
}
.record-event {
transition: all 0.2s ease;
width: 100%;
max-width: 100%;
&:hover {
transform: translateX(2px);
@ -44,9 +330,12 @@
gap: 8px;
flex-wrap: wrap;
align-items: center;
width: 100%;
max-width: 100%;
.ant-btn {
flex-shrink: 0;
max-width: 100%;
}
}
@ -58,71 +347,8 @@
background-color: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #1890ff;
}
// Session management styles
.session-list {
.ant-list-item {
padding: 8px 0;
.ant-card {
transition: all 0.2s ease;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
&.selected-session {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
.ant-card-head {
background-color: #f0f7ff;
}
}
.ant-card-actions {
background-color: #fafafa;
.ant-btn {
border: none;
box-shadow: none;
&:hover {
background-color: #e6f7ff;
}
&.ant-btn-dangerous:hover {
background-color: #fff2f0;
}
}
}
.session-meta {
.session-status {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.session-details {
font-size: 12px;
color: #999;
line-height: 1.4;
}
}
}
}
}
.session-empty {
text-align: center;
padding: 40px 20px;
.ant-empty-description {
color: #999;
}
word-break: break-all;
white-space: normal;
}
// Recording status indicator
@ -192,36 +418,170 @@
margin: 24px 0;
border-color: #e8e8e8;
}
// Events section
.events-section {
.events-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
// 全局禁用可能产生滚动条的 Ant Design 组件
.ant-list {
overflow: visible !important;
max-width: 100% !important;
}
.ant-list-item {
overflow: visible !important;
max-width: 100% !important;
}
.ant-card {
overflow: visible !important;
max-width: 100% !important;
}
.ant-card-body {
overflow: visible !important;
max-width: 100% !important;
}
.ant-timeline {
overflow: visible !important;
max-width: 100% !important;
}
.ant-timeline-item {
overflow: visible !important;
max-width: 100% !important;
}
.ant-space {
overflow: visible !important;
max-width: 100% !important;
}
.ant-empty {
overflow: visible !important;
max-width: 100% !important;
}
// 防止文本和 URL 导致横向滚动
.ant-typography {
word-break: break-all !important;
white-space: normal !important;
max-width: 100% !important;
}
// 确保输入框和按钮不超出容器
.ant-input {
max-width: 100% !important;
}
.ant-btn {
max-width: 100% !important;
word-break: break-all;
}
// 特别针对 RecordTimeline 的样式优化
.record-timeline {
overflow: visible !important;
max-width: 100% !important;
* {
overflow: visible !important;
box-sizing: border-box;
max-width: 100% !important;
}
// 如果内容很长,确保可以完整显示
.ant-timeline-item-content {
overflow: visible !important;
word-break: break-word;
width: 100% !important;
max-width: 100% !important;
}
// 确保时间轴项目充分利用宽度
.ant-timeline-item {
width: 100% !important;
max-width: 100% !important;
}
// 卡片样式优化
.ant-card {
margin-bottom: 8px;
width: 100% !important;
max-width: 100% !important;
.events-title {
font-weight: 600;
font-size: 14px;
}
.events-meta {
font-size: 12px;
color: #999;
.ant-card-body {
padding: 12px 16px;
word-wrap: break-word;
overflow-wrap: break-word;
}
}
.events-container {
min-height: 200px;
// 特别处理长文本和 URL
.ant-space-item {
word-break: break-all;
white-space: normal;
max-width: 100%;
}
// Timeline 组件内部的 Space 组件优化
.ant-space {
width: 100% !important;
max-width: 100% !important;
&.empty {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-style: italic;
.ant-space-item {
max-width: 100% !important;
word-break: break-all !important;
white-space: normal !important;
overflow-wrap: break-word !important;
}
}
// 处理 RecordTimeline 内部的 Typography 组件
.ant-typography {
word-break: break-all !important;
white-space: normal !important;
max-width: 100% !important;
overflow-wrap: break-word !important;
}
// 特别处理可能的长 URL 显示
.ant-typography[type="secondary"] {
word-break: break-all !important;
white-space: normal !important;
max-width: 100% !important;
overflow-wrap: anywhere !important;
}
// 处理 Tag 组件
.ant-tag {
max-width: 100% !important;
word-break: break-all !important;
white-space: normal !important;
}
// 处理 Button 组件
.ant-btn {
max-width: 100% !important;
word-break: break-all !important;
}
// 处理 Code 文本
code {
word-break: break-all !important;
white-space: normal !important;
max-width: 100% !important;
overflow-wrap: break-word !important;
}
// 处理可能的固定宽度元素
img {
max-width: 100% !important;
height: auto !important;
}
// 确保所有文本内容都能正确换行
* {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
}
}

View File

@ -1,5 +1,5 @@
/// <reference types="chrome" />
import { DeleteOutlined, DownloadOutlined, EditOutlined, PlayCircleOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons';
import { ArrowLeftOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, PlayCircleOutlined, PlusOutlined, StopOutlined } from '@ant-design/icons';
import { RecordTimeline } from '@midscene/record';
import { Alert, Button, Card, Divider, Empty, Form, Input, List, Modal, Popconfirm, Space, Tag, Typography, message } from 'antd';
import type React from 'react';
@ -15,6 +15,308 @@ interface RecordMessage {
data?: RecordedEvent | RecordedEvent[];
}
// View modes
type ViewMode = 'list' | 'detail';
// Record List Component
const RecordList: React.FC<{
sessions: RecordingSession[];
currentSessionId: string | null;
onCreateSession: () => void;
onEditSession: (session: RecordingSession) => void;
onDeleteSession: (sessionId: string) => void;
onSelectSession: (session: RecordingSession) => void;
onExportSession: (session: RecordingSession) => void;
onViewDetail: (session: RecordingSession) => void;
}> = ({
sessions,
currentSessionId,
onCreateSession,
onEditSession,
onDeleteSession,
onSelectSession,
onExportSession,
onViewDetail
}) => {
return (
<div className="record-list-view">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<Title level={3} style={{ margin: 0 }}>Recording Sessions</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onCreateSession}
>
New Session
</Button>
</div>
{sessions.length === 0 ? (
<div className="session-empty">
<Empty
description="No recording sessions yet"
style={{ margin: '40px 0' }}
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={onCreateSession}
>
Create First Session
</Button>
</Empty>
</div>
) : (
<List
className="session-list"
grid={{ gutter: 16, column: 1 }}
dataSource={sessions}
renderItem={(session) => (
<List.Item>
<Card
size="small"
className={session.id === currentSessionId ? 'selected-session' : ''}
style={{
cursor: 'pointer',
border: session.id === currentSessionId ? '2px solid #1890ff' : '1px solid #d9d9d9'
}}
onClick={() => onViewDetail(session)}
actions={[
<Button
key="select"
type="text"
size="small"
onClick={(e) => {
e.stopPropagation();
onSelectSession(session);
}}
style={{ color: session.id === currentSessionId ? '#1890ff' : undefined }}
>
{session.id === currentSessionId ? 'Selected' : 'Select'}
</Button>,
<Button
key="edit"
type="text"
icon={<EditOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
onEditSession(session);
}}
/>,
<Button
key="download"
type="text"
icon={<DownloadOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
onExportSession(session);
}}
disabled={session.events.length === 0}
/>,
<Popconfirm
key="delete"
title="Delete session"
description="Are you sure you want to delete this session?"
onConfirm={(e) => {
e?.stopPropagation();
onDeleteSession(session.id);
}}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
size="small"
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
]}
>
<Card.Meta
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{session.name}</span>
<Space>
<Tag color={session.status === 'recording' ? 'red' : session.status === 'completed' ? 'green' : 'default'}>
{session.status}
</Tag>
{session.id === currentSessionId && (
<Tag color="blue">Current</Tag>
)}
</Space>
</div>
}
description={
<div className="session-meta">
{session.description && <div style={{ marginBottom: '4px' }}>{session.description}</div>}
<div className="session-details">
Events: {session.events.length} |
Created: {new Date(session.createdAt).toLocaleString()} |
{session.duration && ` Duration: ${(session.duration / 1000).toFixed(1)}s |`}
{session.url && ` URL: ${session.url.slice(0, 50)}${session.url.length > 50 ? '...' : ''}`}
</div>
</div>
}
/>
</Card>
</List.Item>
)}
/>
)}
</div>
);
};
// Record Detail Component
const RecordDetail: React.FC<{
session: RecordingSession;
events: RecordedEvent[];
isRecording: boolean;
currentTab: chrome.tabs.Tab | null;
onBack: () => void;
onStartRecording: () => void;
onStopRecording: () => void;
onClearEvents: () => void;
onExportEvents: () => void;
}> = ({
session,
events,
isRecording,
currentTab,
onBack,
onStartRecording,
onStopRecording,
onClearEvents,
onExportEvents
}) => {
return (
<div className="record-detail-view">
{/* Header with back button and session info */}
<div className="detail-header">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
className="back-button"
>
Back to Sessions
</Button>
<div className="session-title">
<Title level={4}>{session.name}</Title>
<Text type="secondary">{session.description}</Text>
</div>
</div>
{/* Recording Status Indicator */}
<div className={`recording-status ${isRecording ? 'recording' : 'idle'}`}>
{isRecording ? (
<span>🔴 Recording in progress</span>
) : (
<span> Ready to record</span>
)}
</div>
{/* Session Details */}
<Card size="small" className="session-info-card">
<div className="session-info">
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div>
<Text strong>Status: </Text>
<Tag color={session.status === 'recording' ? 'red' : session.status === 'completed' ? 'green' : 'default'}>
{session.status}
</Tag>
</div>
<div>
<Text strong>Events: </Text>
<Text>{session.events.length}</Text>
</div>
<div>
<Text strong>Created: </Text>
<Text>{new Date(session.createdAt).toLocaleString()}</Text>
</div>
{session.duration && (
<div>
<Text strong>Duration: </Text>
<Text>{(session.duration / 1000).toFixed(1)}s</Text>
</div>
)}
{session.url && (
<div>
<Text strong>URL: </Text>
<Text>{session.url}</Text>
</div>
)}
</Space>
</div>
</Card>
{/* Recording Controls */}
<div className="controls-section">
<div className="current-tab-info">
<Text strong>Current Tab:</Text> {currentTab?.title || 'No tab selected'}
</div>
<Space className="record-controls">
{!isRecording ? (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={onStartRecording}
disabled={!currentTab}
>
Start Recording
</Button>
) : (
<Button
danger
icon={<StopOutlined />}
onClick={onStopRecording}
>
Stop Recording
</Button>
)}
<Button
icon={<DeleteOutlined />}
onClick={onClearEvents}
disabled={events.length === 0 || isRecording}
>
Clear Events
</Button>
<Button
icon={<DownloadOutlined />}
onClick={onExportEvents}
disabled={events.length === 0}
>
Export Events
</Button>
</Space>
</div>
<Divider />
{/* Events Display */}
<div className="events-section">
<div className="events-header">
<Title level={5}>
Recorded Events ({events.length})
</Title>
</div>
<div className={`events-container ${events.length === 0 ? 'empty' : ''}`}>
{events.length === 0 ? (
<Empty description="No events recorded yet" />
) : (
<RecordTimeline events={events} />
)}
</div>
</div>
</div>
);
};
export default function Record() {
const { isRecording, events, setIsRecording, addEvent, clearEvents, setEvents } = useRecordStore();
const {
@ -26,6 +328,11 @@ export default function Record() {
setCurrentSession,
getCurrentSession
} = useRecordingSessionStore();
// View state management
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [selectedSession, setSelectedSession] = useState<RecordingSession | null>(null);
const [currentTab, setCurrentTab] = useState<chrome.tabs.Tab | null>(null);
const [isInjected, setIsInjected] = useState(false);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
@ -202,6 +509,10 @@ export default function Record() {
setIsCreateModalVisible(false);
form.resetFields();
message.success(`Session "${values.name}" created successfully`);
// Switch to detail view for the new session
setSelectedSession(newSession);
setViewMode('detail');
};
// Edit session
@ -224,6 +535,16 @@ export default function Record() {
updatedAt: Date.now()
});
// Update selectedSession if it's the one being edited
if (selectedSession?.id === editingSession.id) {
setSelectedSession({
...editingSession,
name: values.name,
description: values.description,
updatedAt: Date.now()
});
}
setIsEditModalVisible(false);
setEditingSession(null);
editForm.resetFields();
@ -237,10 +558,15 @@ export default function Record() {
setCurrentSession(null);
clearEvents();
}
// If we're viewing the deleted session, go back to list
if (selectedSession?.id === sessionId) {
setViewMode('list');
setSelectedSession(null);
}
message.success('Session deleted successfully');
};
// Select session
// Select session (set as current)
const handleSelectSession = (session: RecordingSession) => {
// Stop current recording if any
if (isRecording) {
@ -257,6 +583,23 @@ export default function Record() {
message.success(`Switched to session "${session.name}"`);
};
// View session detail
const handleViewDetail = (session: RecordingSession) => {
setSelectedSession(session);
setViewMode('detail');
// If not already the current session, switch to it
if (currentSessionId !== session.id) {
handleSelectSession(session);
}
};
// Go back to list view
const handleBackToList = () => {
setViewMode('list');
setSelectedSession(null);
};
// Start recording
const startRecording = async () => {
// Check if there's a current session
@ -338,6 +681,18 @@ export default function Record() {
duration,
updatedAt: Date.now()
});
// Update selectedSession if it's the current one
if (selectedSession?.id === currentSessionId) {
setSelectedSession({
...selectedSession,
status: 'completed',
events: [...events],
duration,
updatedAt: Date.now()
});
}
message.success(`Recording saved to session "${session.name}"`);
}
}
@ -392,215 +747,34 @@ export default function Record() {
message.success('Events exported successfully');
};
const currentSession = getCurrentSession();
return (
<div className="popup-record-container" style={{ padding: '16px' }}>
<Alert
message="Event Recording"
description="Manage recording sessions and capture user interactions on the current tab."
type="info"
style={{ marginBottom: '16px' }}
/>
{/* Recording Status Indicator */}
{currentSession && (
<div className={`recording-status ${isRecording ? 'recording' : 'idle'}`}>
{isRecording ? (
<span>🔴 Recording in progress in session "{currentSession.name}"</span>
) : (
<span> Ready to record in session "{currentSession.name}"</span>
)}
</div>
)}
{/* Session Management Section */}
<div style={{ marginBottom: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<Title level={4} style={{ margin: 0 }}>Recording Sessions</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsCreateModalVisible(true)}
>
New Session
</Button>
</div>
{sessions.length === 0 ? (
<div className="session-empty">
<Empty
description="No recording sessions yet"
style={{ margin: '20px 0' }}
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsCreateModalVisible(true)}
>
Create First Session
</Button>
</Empty>
</div>
) : (
<List
className="session-list"
grid={{ gutter: 16, column: 1 }}
dataSource={sessions}
renderItem={(session) => (
<List.Item>
<Card
size="small"
className={session.id === currentSessionId ? 'selected-session' : ''}
style={{
cursor: 'pointer',
border: session.id === currentSessionId ? '2px solid #1890ff' : '1px solid #d9d9d9'
}}
onClick={() => handleSelectSession(session)}
actions={[
<Button
key="edit"
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleEditSession(session);
}}
/>,
<Button
key="download"
type="text"
icon={<DownloadOutlined />}
onClick={(e) => {
e.stopPropagation();
exportSessionEvents(session);
}}
disabled={session.events.length === 0}
/>,
<Popconfirm
key="delete"
title="Delete session"
description="Are you sure you want to delete this session?"
onConfirm={(e) => {
e?.stopPropagation();
handleDeleteSession(session.id);
}}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
]}
>
<Card.Meta
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{session.name}</span>
<Space>
<Tag color={session.status === 'recording' ? 'red' : session.status === 'completed' ? 'green' : 'default'}>
{session.status}
</Tag>
{session.id === currentSessionId && (
<Tag color="blue">Current</Tag>
)}
</Space>
</div>
}
description={
<div className="session-meta">
{session.description && <div style={{ marginBottom: '4px' }}>{session.description}</div>}
<div className="session-details">
Events: {session.events.length} |
Created: {new Date(session.createdAt).toLocaleString()} |
{session.duration && ` Duration: ${(session.duration / 1000).toFixed(1)}s |`}
{session.url && ` URL: ${session.url.slice(0, 50)}${session.url.length > 50 ? '...' : ''}`}
</div>
</div>
}
/>
</Card>
</List.Item>
)}
<div className="popup-record-container">
{viewMode === 'list' ? (
<RecordList
sessions={sessions}
currentSessionId={currentSessionId}
onCreateSession={() => setIsCreateModalVisible(true)}
onEditSession={handleEditSession}
onDeleteSession={handleDeleteSession}
onSelectSession={handleSelectSession}
onExportSession={exportSessionEvents}
onViewDetail={handleViewDetail}
/>
) : (
selectedSession && (
<RecordDetail
session={selectedSession}
events={events}
isRecording={isRecording}
currentTab={currentTab}
onBack={handleBackToList}
onStartRecording={startRecording}
onStopRecording={stopRecording}
onClearEvents={clearEvents}
onExportEvents={exportEvents}
/>
)}
</div>
<Divider className="section-divider" />
{/* Recording Controls Section */}
<div style={{ marginBottom: '16px' }}>
<div className="current-tab-info" style={{ marginBottom: '12px' }}>
<Text strong>Current Tab:</Text> {currentTab?.title || 'No tab selected'}
{currentSession && (
<div style={{ marginTop: '4px' }}>
<Text strong>Active Session:</Text> {currentSession.name}
</div>
)}
</div>
<Space className="record-controls">
{!isRecording ? (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={startRecording}
disabled={!currentTab || !currentSessionId}
>
Start Recording
</Button>
) : (
<Button
danger
icon={<StopOutlined />}
onClick={stopRecording}
>
Stop Recording
</Button>
)}
<Button
icon={<DeleteOutlined />}
onClick={clearEvents}
disabled={events.length === 0 || isRecording}
>
Clear Events
</Button>
<Button
icon={<DownloadOutlined />}
onClick={exportEvents}
disabled={events.length === 0}
>
Export Current
</Button>
</Space>
</div>
<Divider className="section-divider" />
{/* Events Display Section */}
<div className="events-section">
<div className="events-header">
<div className="events-title">
Current Events ({events.length})
</div>
{currentSession && events.length > 0 && (
<div className="events-meta">
from session: {currentSession.name}
</div>
)}
</div>
<div className={`events-container ${events.length === 0 ? 'empty' : ''}`}>
{events.length === 0 ? (
<div>No events recorded yet</div>
) : (
<RecordTimeline events={events} />
)}
</div>
</div>
)
)}
{/* Create Session Modal */}
<Modal