✨ feat(extension): export history sessions as json
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
|
import { ArrowDownToLine, ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { downloadHistoryExport } from '@/lib/history-export'
|
||||||
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
|
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
|
||||||
|
|
||||||
function timeAgo(ts: number): string {
|
function timeAgo(ts: number): string {
|
||||||
@@ -41,6 +42,11 @@ export function HistoryList({
|
|||||||
setSessions((prev) => prev.filter((s) => s.id !== id))
|
setSessions((prev) => prev.filter((s) => s.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExport = (e: React.MouseEvent, session: SessionRecord) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
downloadHistoryExport(session.task, session.createdAt, session.history)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -103,14 +109,26 @@ export function HistoryList({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delete */}
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => handleDelete(e, session.id)}
|
onClick={(e) => handleExport(e, session)}
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-destructive cursor-pointer shrink-0"
|
className="p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||||
>
|
title="Export history JSON"
|
||||||
<Trash2 className="size-3" />
|
aria-label={`Export history for ${session.task}`}
|
||||||
</button>
|
>
|
||||||
|
<ArrowDownToLine className="size-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleDelete(e, session.id)}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-destructive cursor-pointer"
|
||||||
|
title="Delete history"
|
||||||
|
aria-label={`Delete history for ${session.task}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
60
packages/extension/src/lib/history-export.ts
Normal file
60
packages/extension/src/lib/history-export.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { HistoricalEvent } from '@page-agent/core'
|
||||||
|
|
||||||
|
const EXPORT_FILE_PREFIX = 'page-agent-history'
|
||||||
|
const MAX_TASK_SLUG_LENGTH = 40
|
||||||
|
|
||||||
|
export function serializeHistoryExport(history: HistoricalEvent[]): string {
|
||||||
|
return `${JSON.stringify(history, null, 2)}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildHistoryExportFilename(task: string, createdAt: number): string {
|
||||||
|
const taskSlug = sanitizeTaskForFilename(task)
|
||||||
|
const timestamp = formatTimestampForFilename(createdAt)
|
||||||
|
|
||||||
|
return taskSlug
|
||||||
|
? `${EXPORT_FILE_PREFIX}-${taskSlug}-${timestamp}.json`
|
||||||
|
: `${EXPORT_FILE_PREFIX}-${timestamp}.json`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadHistoryExport(
|
||||||
|
task: string,
|
||||||
|
createdAt: number,
|
||||||
|
history: HistoricalEvent[]
|
||||||
|
): void {
|
||||||
|
const filename = buildHistoryExportFilename(task, createdAt)
|
||||||
|
const content = serializeHistoryExport(history)
|
||||||
|
const blob = new Blob([content], { type: 'application/json;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeTaskForFilename(task: string): string {
|
||||||
|
return task
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
.slice(0, MAX_TASK_SLUG_LENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestampForFilename(createdAt: number): string {
|
||||||
|
const date = new Date(createdAt)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = pad(date.getMonth() + 1)
|
||||||
|
const day = pad(date.getDate())
|
||||||
|
const hours = pad(date.getHours())
|
||||||
|
const minutes = pad(date.getMinutes())
|
||||||
|
const seconds = pad(date.getSeconds())
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(value: number): string {
|
||||||
|
return value.toString().padStart(2, '0')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user