| 1 | import type React from 'react' |
| 2 | import dayjs from 'dayjs' |
| 3 | import { CircleAlert, Loader2, Sparkles } from 'lucide-react' |
| 4 | import { ScrollArea } from '@renderer/components/ui/ScrollArea' |
| 5 | import type { GenerationLogEvent, GenerationRunStatus } from './types' |
| 6 | |
| 7 | export function GenerationLogPanel({ |
| 8 | events, |
| 9 | status, |
| 10 | pageCountLabel, |
| 11 | growingLabel, |
| 12 | failedLabel, |
| 13 | logTitle, |
| 14 | viewportRef, |
| 15 | onViewportScroll |
| 16 | }: { |
| 17 | events: GenerationLogEvent[] |
| 18 | status: GenerationRunStatus |
| 19 | pageCountLabel: string |
| 20 | growingLabel: string |
| 21 | failedLabel: string |
| 22 | logTitle: string |
| 23 | viewportRef?: React.Ref<HTMLDivElement> |
| 24 | onViewportScroll?: React.UIEventHandler<HTMLDivElement> |
| 25 | }): React.JSX.Element { |
| 26 | return ( |
| 27 | <section className="relative flex min-h-0 flex-1 flex-col rounded-lg border border-[#d8ccb5]/72 bg-[#fff9ef]/82 p-2.5 shadow-[0_14px_30px_rgba(78,91,63,0.1)]"> |
| 28 | <div className="mb-2 flex min-h-8 items-center pr-14"> |
| 29 | <div className="flex min-w-0 items-center gap-1.5 text-xs font-semibold text-[#495a3b]"> |
| 30 | <Sparkles className="h-4 w-4 text-[#6f8159]" /> |
| 31 | <span className="min-w-0 truncate">{logTitle}</span> |
| 32 | </div> |
| 33 | <span |
| 34 | className="absolute right-2.5 top-2.5 z-20 inline-flex h-6 min-w-10 items-center justify-center rounded-md border border-[#b8d3a6] bg-[#edf6e8] px-2 text-[11px] font-semibold text-[#365528] shadow-sm" |
| 35 | title={pageCountLabel} |
| 36 | > |
| 37 | {pageCountLabel} |
| 38 | </span> |
| 39 | </div> |
| 40 | |
| 41 | <ScrollArea |
| 42 | className="min-h-0 flex-1 rounded-lg border border-[#e4d9c3]/55 bg-[#fffaf1]/38" |
| 43 | viewportRef={viewportRef} |
| 44 | onViewportScroll={onViewportScroll} |
| 45 | viewportClassName="px-2 py-2" |
| 46 | > |
| 47 | <div className="space-y-2"> |
| 48 | {events.map((event, index) => ( |
| 49 | <div |
| 50 | key={`${event.text}-${index}`} |
| 51 | className="rounded-lg border border-[#e4d9c3]/70 bg-white/46 px-2.5 py-1.5 text-xs leading-5 text-[#5a674c] shadow-[0_6px_14px_rgba(93,107,77,0.06)]" |
| 52 | > |
| 53 | {event.time && ( |
| 54 | <div className="mb-0.5 text-[10px] leading-4 text-[#a09882]"> |
| 55 | {dayjs(event.time).format('HH:mm:ss')} |
| 56 | </div> |
| 57 | )} |
| 58 | <div className="break-words">{event.text}</div> |
| 59 | </div> |
| 60 | ))} |
| 61 | |
| 62 | {status === 'queued' || status === 'running' ? ( |
| 63 | <div className="flex items-center gap-2 rounded-lg border border-[#e4d9c3]/70 bg-white/46 px-2.5 py-1.5 text-xs text-[#a09882] shadow-[0_6px_14px_rgba(93,107,77,0.06)]"> |
| 64 | <Loader2 className="h-3 w-3 shrink-0 animate-spin" /> |
| 65 | <span className="min-w-0 truncate">{growingLabel}</span> |
| 66 | </div> |
| 67 | ) : status === 'failed' || status === 'cancelled' ? ( |
| 68 | <div className="flex items-center gap-2 rounded-lg border border-[#d7b5ae]/70 bg-[#fff8f4]/72 px-2.5 py-1.5 text-xs text-[#93564f] shadow-[0_6px_14px_rgba(93,107,77,0.06)]"> |
| 69 | <CircleAlert className="h-3.5 w-3.5 shrink-0" /> |
| 70 | <span className="min-w-0 truncate">{failedLabel}</span> |
| 71 | </div> |
| 72 | ) : null} |
| 73 | </div> |
| 74 | </ScrollArea> |
| 75 | </section> |
| 76 | ) |
| 77 | } |
| 78 |