@axplusb/kepler 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useMemo } from 'react'
|
|
4
|
+
import useSWR from 'swr'
|
|
5
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
6
|
+
import type { ImportDiff } from '@/types/claude'
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from '@/components/ui/card'
|
|
14
|
+
import { Button } from '@/components/ui/button'
|
|
15
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
16
|
+
import { Calendar } from '@/components/ui/calendar'
|
|
17
|
+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
|
18
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
19
|
+
import { Separator } from '@/components/ui/separator'
|
|
20
|
+
import {
|
|
21
|
+
Download,
|
|
22
|
+
Upload,
|
|
23
|
+
Database,
|
|
24
|
+
Layers,
|
|
25
|
+
History,
|
|
26
|
+
FileJson2,
|
|
27
|
+
AlertTriangle,
|
|
28
|
+
CheckCircle2,
|
|
29
|
+
Loader2,
|
|
30
|
+
CalendarDays,
|
|
31
|
+
} from 'lucide-react'
|
|
32
|
+
import { format } from 'date-fns'
|
|
33
|
+
|
|
34
|
+
const fetcher = (url: string) =>
|
|
35
|
+
fetch(url).then(r => {
|
|
36
|
+
if (!r.ok) throw new Error(`API error ${r.status}`)
|
|
37
|
+
return r.json()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
interface ExportPreview {
|
|
41
|
+
sessionCount: number
|
|
42
|
+
facetCount: number
|
|
43
|
+
historyEntries: number
|
|
44
|
+
hasStatsCache: boolean
|
|
45
|
+
totalSessionsIndexed: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function previewUrl(dateFrom: string, dateTo: string) {
|
|
49
|
+
const p = new URLSearchParams()
|
|
50
|
+
if (dateFrom) p.set('from', dateFrom)
|
|
51
|
+
if (dateTo) p.set('to', dateTo)
|
|
52
|
+
const q = p.toString()
|
|
53
|
+
return `/api/export${q ? `?${q}` : ''}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function ExportPage() {
|
|
57
|
+
const [exporting, setExporting] = useState(false)
|
|
58
|
+
const [exportRange, setExportRange] = useState<{ from?: Date; to?: Date }>({})
|
|
59
|
+
const [exportPickerOpen, setExportPickerOpen] = useState(false)
|
|
60
|
+
const [importDiff, setImportDiff] = useState<ImportDiff | null>(null)
|
|
61
|
+
const [importError, setImportError] = useState('')
|
|
62
|
+
const [importLoading, setImportLoading] = useState(false)
|
|
63
|
+
const [dragging, setDragging] = useState(false)
|
|
64
|
+
const fileRef = useRef<HTMLInputElement>(null)
|
|
65
|
+
|
|
66
|
+
const rangeFromStr = exportRange.from ? format(exportRange.from, 'yyyy-MM-dd') : ''
|
|
67
|
+
const rangeToStr = exportRange.to ? format(exportRange.to, 'yyyy-MM-dd') : ''
|
|
68
|
+
const swrKey = useMemo(() => previewUrl(rangeFromStr, rangeToStr), [rangeFromStr, rangeToStr])
|
|
69
|
+
|
|
70
|
+
const exportPickerLabel =
|
|
71
|
+
exportRange.from && exportRange.to
|
|
72
|
+
? `${format(exportRange.from, 'MMM d')} – ${format(exportRange.to, 'MMM d, yyyy')}`
|
|
73
|
+
: exportRange.from
|
|
74
|
+
? `${format(exportRange.from, 'MMM d, yyyy')} – …`
|
|
75
|
+
: 'Pick date range (optional)'
|
|
76
|
+
const { data: preview, error: previewError, isLoading: previewLoading } = useSWR<ExportPreview>(
|
|
77
|
+
swrKey,
|
|
78
|
+
fetcher,
|
|
79
|
+
{ refreshInterval: 30_000, keepPreviousData: true }
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const dateFilterActive = Boolean(exportRange.from || exportRange.to)
|
|
83
|
+
|
|
84
|
+
async function handleExport() {
|
|
85
|
+
setExporting(true)
|
|
86
|
+
try {
|
|
87
|
+
const body: Record<string, unknown> = {}
|
|
88
|
+
if (rangeFromStr || rangeToStr) {
|
|
89
|
+
body.dateRange = { from: rangeFromStr || undefined, to: rangeToStr || undefined }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const res = await fetch('/api/export', {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify(body),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const data = await res.json()
|
|
99
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
|
100
|
+
const url = URL.createObjectURL(blob)
|
|
101
|
+
const a = document.createElement('a')
|
|
102
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
103
|
+
a.href = url
|
|
104
|
+
a.download = `ccboard-export-${date}.ccboard.json`
|
|
105
|
+
a.click()
|
|
106
|
+
URL.revokeObjectURL(url)
|
|
107
|
+
} finally {
|
|
108
|
+
setExporting(false)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function processFile(file: File) {
|
|
113
|
+
setImportError('')
|
|
114
|
+
setImportDiff(null)
|
|
115
|
+
setImportLoading(true)
|
|
116
|
+
try {
|
|
117
|
+
const text = await file.text()
|
|
118
|
+
const json = JSON.parse(text)
|
|
119
|
+
const res = await fetch('/api/import', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify(json),
|
|
123
|
+
})
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
const err = await res.json()
|
|
126
|
+
setImportError(err.error ?? 'Import failed')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const diff = await res.json() as ImportDiff
|
|
130
|
+
setImportDiff(diff)
|
|
131
|
+
} catch (e) {
|
|
132
|
+
setImportError(String(e))
|
|
133
|
+
} finally {
|
|
134
|
+
setImportLoading(false)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const onDrop = useCallback((e: React.DragEvent) => {
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
setDragging(false)
|
|
141
|
+
const file = e.dataTransfer.files[0]
|
|
142
|
+
if (file) processFile(file)
|
|
143
|
+
}, [])
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex flex-col min-h-screen">
|
|
147
|
+
<TopBar
|
|
148
|
+
title="Export & import"
|
|
149
|
+
subtitle="Download a portable backup of ~/.orca/ analytics or merge data from another machine"
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
<div className="p-6 space-y-6 flex-1">
|
|
153
|
+
{previewError && (
|
|
154
|
+
<Alert variant="destructive">
|
|
155
|
+
<AlertTriangle className="h-4 w-4" />
|
|
156
|
+
<AlertTitle>Could not load export preview</AlertTitle>
|
|
157
|
+
<AlertDescription>{String(previewError)}</AlertDescription>
|
|
158
|
+
</Alert>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Summary — fills the page so it never feels empty */}
|
|
162
|
+
<section className="space-y-3">
|
|
163
|
+
<div>
|
|
164
|
+
<h2 className="text-lg font-semibold tracking-tight">What will be exported</h2>
|
|
165
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
166
|
+
One JSON file includes stats cache, session metadata, facets, and command history. Numbers below respect the optional date range.
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
170
|
+
{previewLoading && !preview ? (
|
|
171
|
+
<>
|
|
172
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
173
|
+
<Skeleton key={i} className="h-[104px] rounded-xl" />
|
|
174
|
+
))}
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
<Card className="shadow-sm">
|
|
179
|
+
<CardHeader className="pb-2">
|
|
180
|
+
<CardDescription className="flex items-center gap-2">
|
|
181
|
+
<Database className="size-4 text-primary" />
|
|
182
|
+
Sessions
|
|
183
|
+
</CardDescription>
|
|
184
|
+
<CardTitle className="text-2xl tabular-nums">
|
|
185
|
+
{preview?.sessionCount ?? '—'}
|
|
186
|
+
</CardTitle>
|
|
187
|
+
</CardHeader>
|
|
188
|
+
<CardContent className="pt-0">
|
|
189
|
+
<p className="text-xs text-muted-foreground">
|
|
190
|
+
{dateFilterActive
|
|
191
|
+
? 'In selected range'
|
|
192
|
+
: `of ${preview?.totalSessionsIndexed ?? '—'} indexed`}
|
|
193
|
+
</p>
|
|
194
|
+
</CardContent>
|
|
195
|
+
</Card>
|
|
196
|
+
<Card className="shadow-sm">
|
|
197
|
+
<CardHeader className="pb-2">
|
|
198
|
+
<CardDescription className="flex items-center gap-2">
|
|
199
|
+
<Layers className="size-4 text-primary" />
|
|
200
|
+
Facets
|
|
201
|
+
</CardDescription>
|
|
202
|
+
<CardTitle className="text-2xl tabular-nums">
|
|
203
|
+
{preview?.facetCount ?? '—'}
|
|
204
|
+
</CardTitle>
|
|
205
|
+
</CardHeader>
|
|
206
|
+
<CardContent className="pt-0">
|
|
207
|
+
<p className="text-xs text-muted-foreground">Linked to exported sessions</p>
|
|
208
|
+
</CardContent>
|
|
209
|
+
</Card>
|
|
210
|
+
<Card className="shadow-sm">
|
|
211
|
+
<CardHeader className="pb-2">
|
|
212
|
+
<CardDescription className="flex items-center gap-2">
|
|
213
|
+
<History className="size-4 text-primary" />
|
|
214
|
+
History rows
|
|
215
|
+
</CardDescription>
|
|
216
|
+
<CardTitle className="text-2xl tabular-nums">
|
|
217
|
+
{preview?.historyEntries ?? '—'}
|
|
218
|
+
</CardTitle>
|
|
219
|
+
</CardHeader>
|
|
220
|
+
<CardContent className="pt-0">
|
|
221
|
+
<p className="text-xs text-muted-foreground">Recent entries (up to 10k)</p>
|
|
222
|
+
</CardContent>
|
|
223
|
+
</Card>
|
|
224
|
+
<Card className="shadow-sm">
|
|
225
|
+
<CardHeader className="pb-2">
|
|
226
|
+
<CardDescription className="flex items-center gap-2">
|
|
227
|
+
<FileJson2 className="size-4 text-primary" />
|
|
228
|
+
Stats cache
|
|
229
|
+
</CardDescription>
|
|
230
|
+
<CardTitle className="text-base font-medium">
|
|
231
|
+
{preview?.hasStatsCache ? (
|
|
232
|
+
<span className="text-emerald-600 dark:text-emerald-400 flex items-center gap-1.5">
|
|
233
|
+
<CheckCircle2 className="size-5 shrink-0" />
|
|
234
|
+
Included
|
|
235
|
+
</span>
|
|
236
|
+
) : (
|
|
237
|
+
<span className="text-muted-foreground">Not found</span>
|
|
238
|
+
)}
|
|
239
|
+
</CardTitle>
|
|
240
|
+
</CardHeader>
|
|
241
|
+
<CardContent className="pt-0">
|
|
242
|
+
<p className="text-xs text-muted-foreground">From ~/.orca/ when available</p>
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</section>
|
|
249
|
+
|
|
250
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:items-stretch">
|
|
251
|
+
{/* Export */}
|
|
252
|
+
<Card className="shadow-sm border-border/80 flex h-full min-h-0 flex-col">
|
|
253
|
+
<CardHeader>
|
|
254
|
+
<div className="flex items-start gap-3">
|
|
255
|
+
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
256
|
+
<Download className="size-5" />
|
|
257
|
+
</div>
|
|
258
|
+
<div className="space-y-1">
|
|
259
|
+
<CardTitle>Export</CardTitle>
|
|
260
|
+
<CardDescription>
|
|
261
|
+
Download <code className="rounded bg-muted px-1 py-0.5 text-xs">.ccboard.json</code> for backup or
|
|
262
|
+
another machine.
|
|
263
|
+
</CardDescription>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</CardHeader>
|
|
267
|
+
<CardContent className="flex flex-1 flex-col gap-5 min-h-0">
|
|
268
|
+
<div className="space-y-2">
|
|
269
|
+
<span className="text-sm font-medium leading-none">Date range (optional)</span>
|
|
270
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
271
|
+
<Popover open={exportPickerOpen} onOpenChange={setExportPickerOpen}>
|
|
272
|
+
<PopoverTrigger asChild>
|
|
273
|
+
<Button
|
|
274
|
+
type="button"
|
|
275
|
+
variant={dateFilterActive ? 'default' : 'outline'}
|
|
276
|
+
size="sm"
|
|
277
|
+
className="gap-2 justify-start"
|
|
278
|
+
>
|
|
279
|
+
<CalendarDays className="w-3.5 h-3.5 shrink-0" />
|
|
280
|
+
<span className="truncate">{exportPickerLabel}</span>
|
|
281
|
+
</Button>
|
|
282
|
+
</PopoverTrigger>
|
|
283
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
284
|
+
<Calendar
|
|
285
|
+
mode="range"
|
|
286
|
+
selected={{ from: exportRange.from, to: exportRange.to }}
|
|
287
|
+
onSelect={range => {
|
|
288
|
+
setExportRange({ from: range?.from, to: range?.to })
|
|
289
|
+
if (range?.from && range?.to) setExportPickerOpen(false)
|
|
290
|
+
}}
|
|
291
|
+
disabled={{ after: new Date() }}
|
|
292
|
+
initialFocus
|
|
293
|
+
/>
|
|
294
|
+
</PopoverContent>
|
|
295
|
+
</Popover>
|
|
296
|
+
{dateFilterActive && (
|
|
297
|
+
<Button
|
|
298
|
+
type="button"
|
|
299
|
+
variant="ghost"
|
|
300
|
+
size="sm"
|
|
301
|
+
className="h-8 text-muted-foreground"
|
|
302
|
+
onClick={() => setExportRange({})}
|
|
303
|
+
>
|
|
304
|
+
Clear
|
|
305
|
+
</Button>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
{dateFilterActive && (
|
|
310
|
+
<p className="text-xs text-muted-foreground rounded-md bg-muted/50 px-3 py-2 border border-border/60">
|
|
311
|
+
Only sessions whose <span className="font-medium text-foreground/80">start time</span> falls in this
|
|
312
|
+
range are included. Use <span className="font-medium text-foreground/80">Clear</span> for a full export.
|
|
313
|
+
</p>
|
|
314
|
+
)}
|
|
315
|
+
<div className="mt-auto flex flex-col gap-5">
|
|
316
|
+
<Separator />
|
|
317
|
+
<Button
|
|
318
|
+
className="w-full sm:w-auto"
|
|
319
|
+
size="lg"
|
|
320
|
+
onClick={handleExport}
|
|
321
|
+
disabled={exporting || previewLoading}
|
|
322
|
+
>
|
|
323
|
+
{exporting ? (
|
|
324
|
+
<>
|
|
325
|
+
<Loader2 className="size-4 animate-spin" />
|
|
326
|
+
Preparing download…
|
|
327
|
+
</>
|
|
328
|
+
) : (
|
|
329
|
+
<>
|
|
330
|
+
<Download className="size-4" />
|
|
331
|
+
Download export
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</Button>
|
|
335
|
+
</div>
|
|
336
|
+
</CardContent>
|
|
337
|
+
</Card>
|
|
338
|
+
|
|
339
|
+
{/* Import */}
|
|
340
|
+
<Card className="shadow-sm border-border/80 flex h-full min-h-0 flex-col">
|
|
341
|
+
<CardHeader>
|
|
342
|
+
<div className="flex items-start gap-3">
|
|
343
|
+
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground">
|
|
344
|
+
<Upload className="size-5" />
|
|
345
|
+
</div>
|
|
346
|
+
<div className="space-y-1">
|
|
347
|
+
<CardTitle>Import / merge</CardTitle>
|
|
348
|
+
<CardDescription>
|
|
349
|
+
Drop a file from another machine. Merge is <strong className="text-foreground/90">additive only</strong>
|
|
350
|
+
— existing sessions are never overwritten.
|
|
351
|
+
</CardDescription>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</CardHeader>
|
|
355
|
+
<CardContent className="flex flex-1 flex-col gap-4 min-h-0">
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
onDragOver={e => {
|
|
359
|
+
e.preventDefault()
|
|
360
|
+
setDragging(true)
|
|
361
|
+
}}
|
|
362
|
+
onDragLeave={() => setDragging(false)}
|
|
363
|
+
onDrop={onDrop}
|
|
364
|
+
onClick={() => fileRef.current?.click()}
|
|
365
|
+
className={`
|
|
366
|
+
flex w-full flex-1 min-h-[220px] flex-col items-center justify-center rounded-xl border-2 border-dashed px-4 py-8 text-center transition-colors
|
|
367
|
+
${dragging ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/40 hover:bg-muted/40'}
|
|
368
|
+
`}
|
|
369
|
+
>
|
|
370
|
+
<Upload className="size-8 mb-3 text-muted-foreground opacity-80" />
|
|
371
|
+
<p className="text-sm text-foreground font-medium">Drop .ccboard.json here</p>
|
|
372
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
373
|
+
or <span className="text-primary underline underline-offset-2">browse files</span>
|
|
374
|
+
</p>
|
|
375
|
+
<input
|
|
376
|
+
ref={fileRef}
|
|
377
|
+
type="file"
|
|
378
|
+
accept=".json,.ccboard.json,application/json"
|
|
379
|
+
className="hidden"
|
|
380
|
+
onChange={e => {
|
|
381
|
+
const file = e.target.files?.[0]
|
|
382
|
+
if (file) processFile(file)
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
</button>
|
|
386
|
+
|
|
387
|
+
{importLoading && (
|
|
388
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
389
|
+
<Loader2 className="size-4 animate-spin" />
|
|
390
|
+
Analyzing file…
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
|
|
394
|
+
{importError && (
|
|
395
|
+
<Alert variant="destructive">
|
|
396
|
+
<AlertTriangle className="h-4 w-4" />
|
|
397
|
+
<AlertDescription className="font-mono text-xs">{importError}</AlertDescription>
|
|
398
|
+
</Alert>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{importDiff && (
|
|
402
|
+
<div className="rounded-lg border bg-muted/30 p-4 space-y-3 text-sm">
|
|
403
|
+
<p className="font-semibold text-foreground flex items-center gap-2">
|
|
404
|
+
<FileJson2 className="size-4 opacity-70" />
|
|
405
|
+
Merge preview
|
|
406
|
+
</p>
|
|
407
|
+
<dl className="grid gap-2 text-muted-foreground">
|
|
408
|
+
<div className="flex justify-between gap-4">
|
|
409
|
+
<dt>Sessions in file</dt>
|
|
410
|
+
<dd className="font-mono text-foreground font-semibold tabular-nums">
|
|
411
|
+
{importDiff.total_in_export}
|
|
412
|
+
</dd>
|
|
413
|
+
</div>
|
|
414
|
+
<div className="flex justify-between gap-4">
|
|
415
|
+
<dt>Already present (skipped)</dt>
|
|
416
|
+
<dd className="font-mono tabular-nums text-muted-foreground/80">{importDiff.already_present}</dd>
|
|
417
|
+
</div>
|
|
418
|
+
<div className="flex justify-between gap-4">
|
|
419
|
+
<dt>New sessions to add</dt>
|
|
420
|
+
<dd
|
|
421
|
+
className={`font-mono font-semibold tabular-nums ${
|
|
422
|
+
importDiff.new_sessions > 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground/60'
|
|
423
|
+
}`}
|
|
424
|
+
>
|
|
425
|
+
{importDiff.new_sessions}
|
|
426
|
+
</dd>
|
|
427
|
+
</div>
|
|
428
|
+
</dl>
|
|
429
|
+
|
|
430
|
+
{importDiff.new_sessions === 0 ? (
|
|
431
|
+
<p className="text-xs text-muted-foreground pt-1 border-t border-border/60">
|
|
432
|
+
Everything in this file is already in your index. Nothing to merge.
|
|
433
|
+
</p>
|
|
434
|
+
) : (
|
|
435
|
+
<div className="space-y-2 pt-1 border-t border-border/60">
|
|
436
|
+
<Alert className="border-amber-500/40 bg-amber-500/5">
|
|
437
|
+
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
|
438
|
+
<AlertDescription className="text-xs text-amber-800 dark:text-amber-200/90">
|
|
439
|
+
Writing merged data to ~/.orca/ is not implemented in this build — this is a preview only.
|
|
440
|
+
</AlertDescription>
|
|
441
|
+
</Alert>
|
|
442
|
+
<div className="max-h-36 overflow-auto rounded-md border border-border/60 bg-background/50 space-y-1 p-2 font-mono text-[11px] text-muted-foreground">
|
|
443
|
+
{importDiff.sessions_to_add.slice(0, 12).map(s => (
|
|
444
|
+
<div key={s.session_id}>
|
|
445
|
+
+ {s.session_id.slice(0, 8)}… · {s.start_time.slice(0, 10)} ·{' '}
|
|
446
|
+
{s.project_path?.split('/').slice(-1)[0] ?? '—'}
|
|
447
|
+
</div>
|
|
448
|
+
))}
|
|
449
|
+
{importDiff.sessions_to_add.length > 12 && (
|
|
450
|
+
<p className="text-muted-foreground/50 pt-1">
|
|
451
|
+
…and {importDiff.sessions_to_add.length - 12} more
|
|
452
|
+
</p>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
</CardContent>
|
|
460
|
+
</Card>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
Binary file
|