@ailog/cli 0.1.2 → 0.1.4
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/dist/bin/cli.js +1 -1
- package/dist/standalone/.next/BUILD_ID +1 -1
- package/dist/standalone/.next/build-manifest.json +2 -2
- package/dist/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/api/[[...route]]/route.js.nft.json +1 -1
- package/dist/standalone/.next/server/app/index.html +1 -1
- package/dist/standalone/.next/server/app/index.rsc +1 -1
- package/dist/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/.next/server/pages/404.html +1 -1
- package/dist/standalone/.next/server/pages/500.html +2 -2
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/layout.tsx +3 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/page.tsx +238 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/settings/page.tsx +224 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/threads/[threadId]/page.tsx +283 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/components/apps-table.tsx +274 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/components.json +24 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/layout.tsx +3 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/page.tsx +238 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/settings/page.tsx +224 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/app/apps/[appId]/threads/[threadId]/page.tsx +283 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/components/apps-table.tsx +274 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/components.json +24 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/package.json +66 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/dist/standalone/tsconfig.json +34 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/package.json +65 -0
- package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/tsconfig.json +34 -0
- package/dist/standalone/dist/standalone/dist/standalone/package.json +1 -2
- package/dist/standalone/dist/standalone/package.json +1 -1
- package/dist/standalone/package.json +1 -1
- package/package.json +1 -1
- /package/dist/standalone/.next/static/{UNT-TmC5XRRFhNzj7jZmZ → HzNPAAEgDfSt2GDQklLOp}/_buildManifest.js +0 -0
- /package/dist/standalone/.next/static/{UNT-TmC5XRRFhNzj7jZmZ → HzNPAAEgDfSt2GDQklLOp}/_clientMiddlewareManifest.json +0 -0
- /package/dist/standalone/.next/static/{UNT-TmC5XRRFhNzj7jZmZ → HzNPAAEgDfSt2GDQklLOp}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { useParams } from 'next/navigation'
|
|
5
|
+
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
8
|
+
import { ChatView } from '@/components/chat-view'
|
|
9
|
+
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
10
|
+
import { RequestDetailPanel } from '@/components/request-detail-panel'
|
|
11
|
+
import { GenerationDetailPanel } from '@/components/generation-detail-panel'
|
|
12
|
+
import { GenerationTimeline } from '@/components/generation-timeline'
|
|
13
|
+
import { useKeydown } from '@/hooks/use-keydown'
|
|
14
|
+
import { rpcClient } from '@/lib/rpc-client'
|
|
15
|
+
import type { Thread, Generation } from '@/lib/api'
|
|
16
|
+
|
|
17
|
+
export default function ThreadDetailPage() {
|
|
18
|
+
const params = useParams()
|
|
19
|
+
const appId = params.appId as string
|
|
20
|
+
const threadId = params.threadId as string
|
|
21
|
+
|
|
22
|
+
const [thread, setThread] = useState<Thread | null>(null)
|
|
23
|
+
const [generations, setGenerations] = useState<Generation[]>([])
|
|
24
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
25
|
+
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(
|
|
26
|
+
null,
|
|
27
|
+
)
|
|
28
|
+
const [selectedGenerationId, setSelectedGenerationId] = useState<
|
|
29
|
+
string | null
|
|
30
|
+
>(null)
|
|
31
|
+
const [viewMode, setViewMode] = useState<'details' | 'timeline'>('details')
|
|
32
|
+
|
|
33
|
+
// Keyboard navigation for generations
|
|
34
|
+
const handleKeyDown = useCallback(
|
|
35
|
+
(e: KeyboardEvent) => {
|
|
36
|
+
if (generations.length === 0) return
|
|
37
|
+
|
|
38
|
+
const currentIndex = generations.findIndex(
|
|
39
|
+
(g) => g.generationId === selectedGenerationId,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (e.key === 'ArrowUp') {
|
|
43
|
+
e.preventDefault()
|
|
44
|
+
if (currentIndex === -1) {
|
|
45
|
+
setSelectedGenerationId(generations[0].generationId)
|
|
46
|
+
setSelectedRequestId(null)
|
|
47
|
+
} else if (currentIndex < generations.length - 1) {
|
|
48
|
+
setSelectedGenerationId(generations[currentIndex + 1].generationId)
|
|
49
|
+
setSelectedRequestId(null)
|
|
50
|
+
}
|
|
51
|
+
} else if (e.key === 'ArrowDown') {
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
if (currentIndex > 0) {
|
|
54
|
+
setSelectedGenerationId(generations[currentIndex - 1].generationId)
|
|
55
|
+
setSelectedRequestId(null)
|
|
56
|
+
} else if (currentIndex === -1 && generations.length > 0) {
|
|
57
|
+
setSelectedGenerationId(
|
|
58
|
+
generations[generations.length - 1].generationId,
|
|
59
|
+
)
|
|
60
|
+
setSelectedRequestId(null)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[generations, selectedGenerationId],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
useKeydown(handleKeyDown)
|
|
68
|
+
|
|
69
|
+
const leftContainerRef = useRef<HTMLDivElement>(null)
|
|
70
|
+
|
|
71
|
+
// Scroll to selected generation (within ScrollArea only, not main page)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!selectedGenerationId || !leftContainerRef.current) return
|
|
74
|
+
|
|
75
|
+
const viewport = leftContainerRef.current.querySelector(
|
|
76
|
+
'[data-slot="scroll-area-viewport"]',
|
|
77
|
+
)
|
|
78
|
+
const element = leftContainerRef.current.querySelector(
|
|
79
|
+
`[data-generation-id="${selectedGenerationId}"]`,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (viewport && element) {
|
|
83
|
+
const viewportRect = viewport.getBoundingClientRect()
|
|
84
|
+
const elementRect = element.getBoundingClientRect()
|
|
85
|
+
const scrollTop = viewport.scrollTop
|
|
86
|
+
|
|
87
|
+
const elementTop = elementRect.top - viewportRect.top + scrollTop
|
|
88
|
+
const elementBottom = elementTop + elementRect.height
|
|
89
|
+
const viewportHeight = viewportRect.height
|
|
90
|
+
|
|
91
|
+
if (elementTop < scrollTop) {
|
|
92
|
+
viewport.scrollTo({ top: elementTop, behavior: 'smooth' })
|
|
93
|
+
} else if (elementBottom > scrollTop + viewportHeight) {
|
|
94
|
+
viewport.scrollTo({
|
|
95
|
+
top: elementBottom - viewportHeight,
|
|
96
|
+
behavior: 'smooth',
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [selectedGenerationId])
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
async function fetchData() {
|
|
104
|
+
try {
|
|
105
|
+
// Fetch thread and all generations for this thread
|
|
106
|
+
const [threadRes, generationsRes] = await Promise.all([
|
|
107
|
+
rpcClient.api.admin.apps[':appId'].threads[':threadId'].$get({
|
|
108
|
+
param: { appId, threadId },
|
|
109
|
+
}),
|
|
110
|
+
rpcClient.api.admin.apps[':appId'].generations.$get({
|
|
111
|
+
param: { appId },
|
|
112
|
+
query: { threadId },
|
|
113
|
+
}),
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
if (threadRes.ok) {
|
|
117
|
+
const threadData = await threadRes.json()
|
|
118
|
+
setThread(threadData as Thread)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (generationsRes.ok) {
|
|
122
|
+
const generationsData = await generationsRes.json()
|
|
123
|
+
setGenerations(generationsData.generations as Generation[])
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Failed to fetch data:', error)
|
|
127
|
+
} finally {
|
|
128
|
+
setIsLoading(false)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
fetchData()
|
|
132
|
+
}, [appId, threadId])
|
|
133
|
+
|
|
134
|
+
// Derive requests from generations by grouping unique requestIds
|
|
135
|
+
const requests = useMemo(() => {
|
|
136
|
+
const requestMap = new Map<
|
|
137
|
+
string,
|
|
138
|
+
{ requestId: string; createdAt: string }
|
|
139
|
+
>()
|
|
140
|
+
for (const gen of generations) {
|
|
141
|
+
if (!requestMap.has(gen.requestId)) {
|
|
142
|
+
requestMap.set(gen.requestId, {
|
|
143
|
+
requestId: gen.requestId,
|
|
144
|
+
createdAt: gen.createdAt,
|
|
145
|
+
})
|
|
146
|
+
} else {
|
|
147
|
+
// Use the earliest createdAt for the request
|
|
148
|
+
const existing = requestMap.get(gen.requestId)!
|
|
149
|
+
if (gen.createdAt < existing.createdAt) {
|
|
150
|
+
existing.createdAt = gen.createdAt
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return Array.from(requestMap.values())
|
|
155
|
+
}, [generations])
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="space-y-4">
|
|
159
|
+
{/* Thread Info / Timeline */}
|
|
160
|
+
<Card className="py-2">
|
|
161
|
+
<CardHeader className="px-3 py-2">
|
|
162
|
+
<div className="flex items-center justify-between">
|
|
163
|
+
<div className="flex gap-1">
|
|
164
|
+
<Button
|
|
165
|
+
variant={viewMode === 'details' ? 'secondary' : 'ghost'}
|
|
166
|
+
size="sm"
|
|
167
|
+
className="h-6 px-2 text-xs"
|
|
168
|
+
onClick={() => setViewMode('details')}
|
|
169
|
+
>
|
|
170
|
+
Details
|
|
171
|
+
</Button>
|
|
172
|
+
<Button
|
|
173
|
+
variant={viewMode === 'timeline' ? 'secondary' : 'ghost'}
|
|
174
|
+
size="sm"
|
|
175
|
+
className="h-6 px-2 text-xs"
|
|
176
|
+
onClick={() => setViewMode('timeline')}
|
|
177
|
+
disabled={generations.length === 0}
|
|
178
|
+
>
|
|
179
|
+
Timeline
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
{isLoading ? (
|
|
183
|
+
<Skeleton className="h-4 w-24" />
|
|
184
|
+
) : (
|
|
185
|
+
<span className="text-muted-foreground text-xs">
|
|
186
|
+
{thread?.title || '(Untitled)'}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</CardHeader>
|
|
191
|
+
<CardContent className="px-3 pt-0 pb-2">
|
|
192
|
+
{viewMode === 'details' ? (
|
|
193
|
+
isLoading ? (
|
|
194
|
+
<div className="flex gap-6">
|
|
195
|
+
<Skeleton className="h-3 w-24" />
|
|
196
|
+
<Skeleton className="h-3 w-24" />
|
|
197
|
+
</div>
|
|
198
|
+
) : (
|
|
199
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs">
|
|
200
|
+
<div>
|
|
201
|
+
<span className="text-muted-foreground">Thread ID: </span>
|
|
202
|
+
<span className="font-mono">{thread?.threadId}</span>
|
|
203
|
+
</div>
|
|
204
|
+
{thread?.tenantId && (
|
|
205
|
+
<div>
|
|
206
|
+
<span className="text-muted-foreground">Tenant ID: </span>
|
|
207
|
+
<span>{thread.tenantId}</span>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
<div>
|
|
211
|
+
<span className="text-muted-foreground">User: </span>
|
|
212
|
+
<span className="font-mono">{thread?.userId}</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
) : (
|
|
217
|
+
<GenerationTimeline
|
|
218
|
+
generations={generations}
|
|
219
|
+
selectedGenerationId={selectedGenerationId}
|
|
220
|
+
onSelectGeneration={(generationId) => {
|
|
221
|
+
setSelectedGenerationId(generationId)
|
|
222
|
+
setSelectedRequestId(null)
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</CardContent>
|
|
227
|
+
</Card>
|
|
228
|
+
|
|
229
|
+
{/* Split View: Chat + Details */}
|
|
230
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-3">
|
|
231
|
+
<ScrollArea
|
|
232
|
+
className="h-[calc(100vh-220px)] [&_[data-slot=scroll-area-viewport]>div]:!block"
|
|
233
|
+
ref={leftContainerRef}
|
|
234
|
+
>
|
|
235
|
+
{/* Left: Chat View */}
|
|
236
|
+
<ChatView
|
|
237
|
+
requests={requests}
|
|
238
|
+
generations={generations}
|
|
239
|
+
isLoading={isLoading}
|
|
240
|
+
selectedRequestId={selectedRequestId}
|
|
241
|
+
selectedGenerationId={selectedGenerationId}
|
|
242
|
+
onSelectRequest={(requestId) => {
|
|
243
|
+
setSelectedRequestId(requestId)
|
|
244
|
+
setSelectedGenerationId(null)
|
|
245
|
+
}}
|
|
246
|
+
onSelectGeneration={(generationId) => {
|
|
247
|
+
setSelectedGenerationId(generationId)
|
|
248
|
+
setSelectedRequestId(null)
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</ScrollArea>
|
|
252
|
+
|
|
253
|
+
<ScrollArea className="h-[calc(100vh-220px)] border [&_[data-slot=scroll-area-viewport]>div]:!block">
|
|
254
|
+
{/* Right: Details Panel */}
|
|
255
|
+
<div className="w-full min-w-0 overflow-hidden p-4">
|
|
256
|
+
{selectedGenerationId ? (
|
|
257
|
+
<GenerationDetailPanel
|
|
258
|
+
generation={
|
|
259
|
+
generations.find(
|
|
260
|
+
(g) => g.generationId === selectedGenerationId,
|
|
261
|
+
) || null
|
|
262
|
+
}
|
|
263
|
+
/>
|
|
264
|
+
) : (
|
|
265
|
+
<RequestDetailPanel
|
|
266
|
+
request={
|
|
267
|
+
requests.find((r) => r.requestId === selectedRequestId) ||
|
|
268
|
+
null
|
|
269
|
+
}
|
|
270
|
+
generations={generations}
|
|
271
|
+
selectedGenerationId={selectedGenerationId}
|
|
272
|
+
onSelectGeneration={(generationId) => {
|
|
273
|
+
setSelectedGenerationId(generationId)
|
|
274
|
+
setSelectedRequestId(null)
|
|
275
|
+
}}
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</ScrollArea>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
package/dist/standalone/dist/standalone/dist/standalone/dist/standalone/components/apps-table.tsx
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from '@/components/ui/table'
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
} from '@/components/ui/dropdown-menu'
|
|
20
|
+
import {
|
|
21
|
+
AlertDialog,
|
|
22
|
+
AlertDialogAction,
|
|
23
|
+
AlertDialogCancel,
|
|
24
|
+
AlertDialogContent,
|
|
25
|
+
AlertDialogDescription,
|
|
26
|
+
AlertDialogFooter,
|
|
27
|
+
AlertDialogHeader,
|
|
28
|
+
AlertDialogTitle,
|
|
29
|
+
} from '@/components/ui/alert-dialog'
|
|
30
|
+
import { Button } from '@/components/ui/button'
|
|
31
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
32
|
+
import {
|
|
33
|
+
Tooltip,
|
|
34
|
+
TooltipContent,
|
|
35
|
+
TooltipTrigger,
|
|
36
|
+
} from '@/components/ui/tooltip'
|
|
37
|
+
import { HugeiconsIcon } from '@hugeicons/react'
|
|
38
|
+
import {
|
|
39
|
+
MoreHorizontalCircle01Icon,
|
|
40
|
+
EyeIcon,
|
|
41
|
+
Delete02Icon,
|
|
42
|
+
Copy01Icon,
|
|
43
|
+
} from '@hugeicons/core-free-icons'
|
|
44
|
+
import { rpcClient } from '@/lib/rpc-client'
|
|
45
|
+
import { toast } from 'sonner'
|
|
46
|
+
import type { App } from '@/lib/api'
|
|
47
|
+
|
|
48
|
+
interface AppsTableProps {
|
|
49
|
+
apps: App[]
|
|
50
|
+
isLoading: boolean
|
|
51
|
+
onDelete: () => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatDate(dateString: string) {
|
|
55
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
56
|
+
year: 'numeric',
|
|
57
|
+
month: 'short',
|
|
58
|
+
day: 'numeric',
|
|
59
|
+
hour: '2-digit',
|
|
60
|
+
minute: '2-digit',
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function copyToClipboard(text: string) {
|
|
65
|
+
navigator.clipboard.writeText(text)
|
|
66
|
+
toast.success('Copied to clipboard')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function truncateId(id: string, maxLength = 12) {
|
|
70
|
+
return id.length > maxLength ? `${id.substring(0, maxLength)}...` : id
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function AppsTable({ apps, isLoading, onDelete }: AppsTableProps) {
|
|
74
|
+
const router = useRouter()
|
|
75
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
|
76
|
+
const [appToDelete, setAppToDelete] = useState<App | null>(null)
|
|
77
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
78
|
+
|
|
79
|
+
const handleDelete = async () => {
|
|
80
|
+
if (!appToDelete) return
|
|
81
|
+
|
|
82
|
+
setIsDeleting(true)
|
|
83
|
+
try {
|
|
84
|
+
const res = await rpcClient.api.admin.apps[':appId'].$delete({
|
|
85
|
+
param: { appId: appToDelete.appId },
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
throw new Error('Failed to delete app')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
toast.success('App deleted successfully')
|
|
93
|
+
setDeleteDialogOpen(false)
|
|
94
|
+
setAppToDelete(null)
|
|
95
|
+
onDelete()
|
|
96
|
+
} catch (error) {
|
|
97
|
+
toast.error('Failed to delete app')
|
|
98
|
+
console.error(error)
|
|
99
|
+
} finally {
|
|
100
|
+
setIsDeleting(false)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isLoading) {
|
|
105
|
+
return (
|
|
106
|
+
<div className="rounded-md border">
|
|
107
|
+
<Table>
|
|
108
|
+
<TableHeader>
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableHead>App ID</TableHead>
|
|
111
|
+
<TableHead>Name</TableHead>
|
|
112
|
+
<TableHead>Created</TableHead>
|
|
113
|
+
<TableHead>Updated</TableHead>
|
|
114
|
+
<TableHead className="w-[70px]"></TableHead>
|
|
115
|
+
</TableRow>
|
|
116
|
+
</TableHeader>
|
|
117
|
+
<TableBody>
|
|
118
|
+
{[...Array(3)].map((_, i) => (
|
|
119
|
+
<TableRow key={i}>
|
|
120
|
+
<TableCell>
|
|
121
|
+
<Skeleton className="h-5 w-48" />
|
|
122
|
+
</TableCell>
|
|
123
|
+
<TableCell>
|
|
124
|
+
<Skeleton className="h-5 w-32" />
|
|
125
|
+
</TableCell>
|
|
126
|
+
<TableCell>
|
|
127
|
+
<Skeleton className="h-5 w-36" />
|
|
128
|
+
</TableCell>
|
|
129
|
+
<TableCell>
|
|
130
|
+
<Skeleton className="h-5 w-36" />
|
|
131
|
+
</TableCell>
|
|
132
|
+
<TableCell>
|
|
133
|
+
<Skeleton className="h-8 w-8" />
|
|
134
|
+
</TableCell>
|
|
135
|
+
</TableRow>
|
|
136
|
+
))}
|
|
137
|
+
</TableBody>
|
|
138
|
+
</Table>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (apps.length === 0) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="rounded-md border p-8 text-center">
|
|
146
|
+
<p className="text-muted-foreground">
|
|
147
|
+
No apps yet. Create your first app to get started.
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div className="rounded-md border">
|
|
156
|
+
<Table>
|
|
157
|
+
<TableHeader>
|
|
158
|
+
<TableRow>
|
|
159
|
+
<TableHead>App ID</TableHead>
|
|
160
|
+
<TableHead>Name</TableHead>
|
|
161
|
+
<TableHead>Created</TableHead>
|
|
162
|
+
<TableHead>Updated</TableHead>
|
|
163
|
+
<TableHead className="w-[70px]"></TableHead>
|
|
164
|
+
</TableRow>
|
|
165
|
+
</TableHeader>
|
|
166
|
+
<TableBody>
|
|
167
|
+
{apps.map((app) => (
|
|
168
|
+
<TableRow
|
|
169
|
+
key={app.appId}
|
|
170
|
+
className="cursor-pointer"
|
|
171
|
+
onClick={() => router.push(`/apps/${app.appId}`)}
|
|
172
|
+
>
|
|
173
|
+
<TableCell>
|
|
174
|
+
<div className="flex items-center gap-2">
|
|
175
|
+
{app.appId.length > 12 ? (
|
|
176
|
+
<Tooltip>
|
|
177
|
+
<TooltipTrigger asChild>
|
|
178
|
+
<code className="text-muted-foreground cursor-default font-mono text-sm">
|
|
179
|
+
{truncateId(app.appId)}
|
|
180
|
+
</code>
|
|
181
|
+
</TooltipTrigger>
|
|
182
|
+
<TooltipContent>{app.appId}</TooltipContent>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
) : (
|
|
185
|
+
<code className="text-muted-foreground font-mono text-sm">
|
|
186
|
+
{app.appId}
|
|
187
|
+
</code>
|
|
188
|
+
)}
|
|
189
|
+
<Button
|
|
190
|
+
variant="ghost"
|
|
191
|
+
size="icon"
|
|
192
|
+
className="h-6 w-6"
|
|
193
|
+
onClick={(e) => {
|
|
194
|
+
e.stopPropagation()
|
|
195
|
+
copyToClipboard(app.appId)
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<HugeiconsIcon icon={Copy01Icon} className="h-3 w-3" />
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
</TableCell>
|
|
202
|
+
<TableCell className="font-medium">{app.name}</TableCell>
|
|
203
|
+
<TableCell className="text-muted-foreground">
|
|
204
|
+
{formatDate(app.createdAt)}
|
|
205
|
+
</TableCell>
|
|
206
|
+
<TableCell className="text-muted-foreground">
|
|
207
|
+
{formatDate(app.updatedAt)}
|
|
208
|
+
</TableCell>
|
|
209
|
+
<TableCell className="p-0" onClick={(e) => e.stopPropagation()}>
|
|
210
|
+
<DropdownMenu>
|
|
211
|
+
<DropdownMenuTrigger asChild>
|
|
212
|
+
<button className="hover:bg-muted/50 flex h-full w-full items-center justify-center p-4">
|
|
213
|
+
<HugeiconsIcon
|
|
214
|
+
icon={MoreHorizontalCircle01Icon}
|
|
215
|
+
className="h-4 w-4"
|
|
216
|
+
/>
|
|
217
|
+
</button>
|
|
218
|
+
</DropdownMenuTrigger>
|
|
219
|
+
<DropdownMenuContent align="end">
|
|
220
|
+
<DropdownMenuItem asChild>
|
|
221
|
+
<Link href={`/apps/${app.appId}`}>
|
|
222
|
+
<HugeiconsIcon
|
|
223
|
+
icon={EyeIcon}
|
|
224
|
+
className="mr-2 h-4 w-4"
|
|
225
|
+
/>
|
|
226
|
+
View
|
|
227
|
+
</Link>
|
|
228
|
+
</DropdownMenuItem>
|
|
229
|
+
<DropdownMenuItem
|
|
230
|
+
className="text-destructive focus:text-destructive"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
setAppToDelete(app)
|
|
233
|
+
setDeleteDialogOpen(true)
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<HugeiconsIcon
|
|
237
|
+
icon={Delete02Icon}
|
|
238
|
+
className="mr-2 h-4 w-4"
|
|
239
|
+
/>
|
|
240
|
+
Delete
|
|
241
|
+
</DropdownMenuItem>
|
|
242
|
+
</DropdownMenuContent>
|
|
243
|
+
</DropdownMenu>
|
|
244
|
+
</TableCell>
|
|
245
|
+
</TableRow>
|
|
246
|
+
))}
|
|
247
|
+
</TableBody>
|
|
248
|
+
</Table>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
252
|
+
<AlertDialogContent>
|
|
253
|
+
<AlertDialogHeader>
|
|
254
|
+
<AlertDialogTitle>Delete App</AlertDialogTitle>
|
|
255
|
+
<AlertDialogDescription>
|
|
256
|
+
Are you sure you want to delete "{appToDelete?.name}"?
|
|
257
|
+
This action cannot be undone and will delete all associated data.
|
|
258
|
+
</AlertDialogDescription>
|
|
259
|
+
</AlertDialogHeader>
|
|
260
|
+
<AlertDialogFooter>
|
|
261
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
262
|
+
<AlertDialogAction
|
|
263
|
+
onClick={handleDelete}
|
|
264
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
265
|
+
disabled={isDeleting}
|
|
266
|
+
>
|
|
267
|
+
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
268
|
+
</AlertDialogAction>
|
|
269
|
+
</AlertDialogFooter>
|
|
270
|
+
</AlertDialogContent>
|
|
271
|
+
</AlertDialog>
|
|
272
|
+
</>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-lyra",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "hugeicons",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"menuColor": "default",
|
|
22
|
+
"menuAccent": "subtle",
|
|
23
|
+
"registries": {}
|
|
24
|
+
}
|