feat: 优化 AI 日报结果中心信息架构

This commit is contained in:
电摇小子
2026-07-14 11:07:35 +08:00
committed by 电摇小子
parent c291c94bd8
commit d28b579cb4
11 changed files with 372 additions and 102 deletions
+9 -1
View File
@@ -10,7 +10,11 @@ import { StickerService } from './sticker-service'
import { parseMessageContent } from './message-parser' import { parseMessageContent } from './message-parser'
import { ImageDecryptService } from './image-decrypt-service' import { ImageDecryptService } from './image-decrypt-service'
import { exportGroupReport } from './group-report-service' import { exportGroupReport } from './group-report-service'
import { listGeneratedReports, saveGeneratedReport } from './report-history-service' import {
deleteGeneratedReport,
listGeneratedReports,
saveGeneratedReport
} from './report-history-service'
import type { GroupReportExportRequest } from '../shared/group-report' import type { GroupReportExportRequest } from '../shared/group-report'
import type { SaveGeneratedReportRequest } from '../shared/report-history' import type { SaveGeneratedReportRequest } from '../shared/report-history'
import { DatabaseKeyStore } from './database-key-store' import { DatabaseKeyStore } from './database-key-store'
@@ -343,6 +347,10 @@ app.whenReady().then(async () => {
return saveGeneratedReport(request) return saveGeneratedReport(request)
}) })
ipcMain.handle('report:deleteGenerated', async (_, reportId: string) => {
return deleteGeneratedReport(reportId)
})
ipcMain.handle('report:reveal', async (_, filePath: string) => { ipcMain.handle('report:reveal', async (_, filePath: string) => {
try { try {
shell.showItemInFolder(filePath) shell.showItemInFolder(filePath)
+33
View File
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs'
import path from 'path' import path from 'path'
import type { import type {
GeneratedReportRecord, GeneratedReportRecord,
DeleteGeneratedReportResult,
ReportAssetStatus, ReportAssetStatus,
ReportHistoryResult, ReportHistoryResult,
SaveGeneratedReportRequest, SaveGeneratedReportRequest,
@@ -166,3 +167,35 @@ export async function saveGeneratedReport(
return { success: false, error: error instanceof Error ? error.message : String(error) } return { success: false, error: error instanceof Error ? error.message : String(error) }
} }
} }
export async function deleteGeneratedReport(reportId: string): Promise<DeleteGeneratedReportResult> {
try {
const jsonFiles = await walkJsonFiles(getReportsRoot())
for (const jsonPath of jsonFiles) {
try {
const content = await fs.readFile(jsonPath, 'utf8')
const record = JSON.parse(content) as GeneratedReportRecord
if (record.id !== reportId) continue
const paths = [record.htmlPath, record.pngPath, jsonPath].filter(
(filePath): filePath is string => Boolean(filePath)
)
await Promise.all(
paths.map(async (filePath) => {
try {
await fs.unlink(filePath)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
}
})
)
return { success: true, deletedId: reportId }
} catch (error) {
console.warn(`[ReportHistory] skip invalid report record while deleting: ${jsonPath}`, error)
}
}
return { success: false, error: '未找到要删除的日报记录' }
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
}
+2
View File
@@ -2,6 +2,7 @@ import { ElectronAPI } from '@electron-toolkit/preload'
import { Contact, Message } from '../shared/types' import { Contact, Message } from '../shared/types'
import { GroupReportExportRequest, GroupReportExportResult } from '../shared/group-report' import { GroupReportExportRequest, GroupReportExportResult } from '../shared/group-report'
import { import {
DeleteGeneratedReportResult,
ReportHistoryResult, ReportHistoryResult,
SaveGeneratedReportRequest, SaveGeneratedReportRequest,
SaveGeneratedReportResult SaveGeneratedReportResult
@@ -93,6 +94,7 @@ declare global {
saveGeneratedReport: ( saveGeneratedReport: (
request: SaveGeneratedReportRequest request: SaveGeneratedReportRequest
) => Promise<SaveGeneratedReportResult> ) => Promise<SaveGeneratedReportResult>
deleteGeneratedReport: (reportId: string) => Promise<DeleteGeneratedReportResult>
revealGroupReport: (filePath: string) => Promise<{ success: boolean; error?: string }> revealGroupReport: (filePath: string) => Promise<{ success: boolean; error?: string }>
getSavedDbKey: () => Promise<{ success: boolean; key?: string; error?: string }> getSavedDbKey: () => Promise<{ success: boolean; key?: string; error?: string }>
autoGetDbKey: () => Promise<{ autoGetDbKey: () => Promise<{
+1
View File
@@ -40,6 +40,7 @@ const api = {
listGeneratedReports: () => ipcRenderer.invoke('report:listGenerated'), listGeneratedReports: () => ipcRenderer.invoke('report:listGenerated'),
saveGeneratedReport: (request: SaveGeneratedReportRequest) => saveGeneratedReport: (request: SaveGeneratedReportRequest) =>
ipcRenderer.invoke('report:saveGenerated', request), ipcRenderer.invoke('report:saveGenerated', request),
deleteGeneratedReport: (reportId: string) => ipcRenderer.invoke('report:deleteGenerated', reportId),
revealGroupReport: (filePath: string) => ipcRenderer.invoke('report:reveal', filePath), revealGroupReport: (filePath: string) => ipcRenderer.invoke('report:reveal', filePath),
getSavedDbKey: () => ipcRenderer.invoke('key:getSavedDbKey'), getSavedDbKey: () => ipcRenderer.invoke('key:getSavedDbKey'),
autoGetDbKey: () => ipcRenderer.invoke('key:autoGetDbKey'), autoGetDbKey: () => ipcRenderer.invoke('key:autoGetDbKey'),
+23 -5
View File
@@ -216,7 +216,9 @@ function App(): React.ReactElement {
} }
const reports = result.reports || [] const reports = result.reports || []
setGeneratedReports(reports) setGeneratedReports(reports)
setSelectedReportId((current) => current || reports[0]?.id || null) setSelectedReportId((current) =>
current && reports.some((report) => report.id === current) ? current : null
)
} catch (error) { } catch (error) {
setReportNotice(error instanceof Error ? error.message : String(error)) setReportNotice(error instanceof Error ? error.message : String(error))
window.setTimeout(() => setReportNotice(''), 3200) window.setTimeout(() => setReportNotice(''), 3200)
@@ -783,7 +785,8 @@ function App(): React.ReactElement {
} }
if (page === 'report') { if (page === 'report') {
setReportWorkspaceView('result') setReportWorkspaceView('result')
if (!selectedReportId && generatedReports[0]) setSelectedReportId(generatedReports[0].id) setSelectedReportId(null)
setSelectedReportImageSize(null)
} }
} }
@@ -876,10 +879,9 @@ function App(): React.ReactElement {
]) ])
const selectedReport = const selectedReport =
generatedReports.find((report) => report.id === selectedReportId) || generatedReports[0] || null generatedReports.find((report) => report.id === selectedReportId) || null
const openReportResult = (): void => { const openReportResult = (): void => {
if (generatedReports[0] && !selectedReportId) setSelectedReportId(generatedReports[0].id)
setReportWorkspaceView('result') setReportWorkspaceView('result')
} }
@@ -910,6 +912,20 @@ function App(): React.ReactElement {
return window.api.revealGroupReport(filePath) return window.api.revealGroupReport(filePath)
} }
const handleDeleteReport = async (
reportId: string
): Promise<{ success: boolean; error?: string }> => {
const result = await window.api.deleteGeneratedReport(reportId)
if (!result.success) return { success: false, error: result.error || '删除日报失败' }
setGeneratedReports((current) => current.filter((report) => report.id !== reportId))
if (selectedReportId === reportId) {
setSelectedReportId(null)
setSelectedReportImageSize(null)
}
return { success: true }
}
const renderPlaceholderPage = (page: Exclude<AppPage, 'archive' | 'report'>): React.ReactElement => { const renderPlaceholderPage = (page: Exclude<AppPage, 'archive' | 'report'>): React.ReactElement => {
const labels: Record<Exclude<AppPage, 'archive' | 'report'>, string> = { const labels: Record<Exclude<AppPage, 'archive' | 'report'>, string> = {
search: '检索', search: '检索',
@@ -963,7 +979,7 @@ function App(): React.ReactElement {
<div className="report-center-page"> <div className="report-center-page">
<ReportHistorySidebar <ReportHistorySidebar
reports={generatedReports} reports={generatedReports}
selectedReportId={selectedReport?.id || null} selectedReportId={selectedReportId}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={isAuthenticated} dbReady={isAuthenticated}
onSelectReport={(reportId) => { onSelectReport={(reportId) => {
@@ -971,10 +987,12 @@ function App(): React.ReactElement {
setSelectedReportImageSize(null) setSelectedReportImageSize(null)
}} }}
onCreateReport={openReportConfigure} onCreateReport={openReportConfigure}
onDeleteReport={handleDeleteReport}
onOpenSettings={() => setShowSettings(true)} onOpenSettings={() => setShowSettings(true)}
/> />
<ReportViewer <ReportViewer
report={selectedReport} report={selectedReport}
hasReports={generatedReports.length > 0}
onBackToConfigure={openReportConfigure} onBackToConfigure={openReportConfigure}
onRegenerate={handleRegenerateReport} onRegenerate={handleRegenerateReport}
onCopyImage={handleCopyReportImage} onCopyImage={handleCopyReportImage}
+129 -21
View File
@@ -3526,28 +3526,16 @@ body {
font: 13px/18px var(--wxex-font); font: 13px/18px var(--wxex-font);
} }
.report-history-filters { .report-history-create {
display: grid;
flex: 0 0 auto; flex: 0 0 auto;
grid-template-columns: repeat(4, minmax(0, 1fr)); height: 36px;
gap: 6px; margin: 0 14px 12px;
padding: 0 14px 10px; border: 1px solid var(--wxex-brand);
} border-radius: var(--wxex-radius-md);
background: var(--wxex-brand);
.report-history-filters button { color: #fff;
height: 28px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
cursor: pointer; cursor: pointer;
font: 12px/16px var(--wxex-font); font: 700 13px/18px var(--wxex-font);
}
.report-history-filters button.active {
border-color: var(--wxex-brand);
background: var(--wxex-brand-soft);
color: var(--wxex-brand);
} }
.report-history-list { .report-history-list {
@@ -3557,6 +3545,23 @@ body {
padding: 0 10px 12px; padding: 0 10px 12px;
} }
.report-history-list-title {
padding: 4px 4px 8px;
color: var(--wxex-text-secondary);
font: 700 13px/18px var(--wxex-font);
}
.report-history-group {
margin: 0 0 10px;
}
.report-history-group h3 {
margin: 0;
padding: 6px 4px;
color: var(--wxex-text-muted);
font: 700 12px/16px var(--wxex-font);
}
.report-history-item { .report-history-item {
position: relative; position: relative;
display: flex; display: flex;
@@ -3614,6 +3619,7 @@ body {
.report-history-text { .report-history-text {
display: grid; display: grid;
flex: 1 1 auto;
min-width: 0; min-width: 0;
gap: 2px; gap: 2px;
} }
@@ -3637,6 +3643,28 @@ body {
font: normal 12px/16px var(--wxex-font); font: normal 12px/16px var(--wxex-font);
} }
.report-history-delete {
display: grid;
width: 38px;
height: 26px;
flex: 0 0 auto;
place-items: center;
align-self: center;
border: 1px solid rgba(198, 72, 72, 0.22);
border-radius: var(--wxex-radius-sm);
background: rgba(198, 72, 72, 0.06);
color: #a64242;
cursor: pointer;
font: 600 12px/16px var(--wxex-font);
opacity: 0;
transition: opacity 0.12s ease;
}
.report-history-item:hover .report-history-delete,
.report-history-delete:focus {
opacity: 1;
}
.report-history-empty { .report-history-empty {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -3664,6 +3692,65 @@ body {
border-top: 1px solid var(--wxex-border); border-top: 1px solid var(--wxex-border);
} }
.report-delete-confirm {
position: fixed;
inset: 0;
z-index: 30;
display: grid;
place-items: center;
background: rgba(32, 39, 36, 0.28);
}
.report-delete-confirm-card {
display: grid;
width: min(360px, calc(100vw - 48px));
gap: 12px;
padding: 18px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-lg);
background: var(--wxex-bg-main);
box-shadow: var(--wxex-shadow-popover);
}
.report-delete-confirm-card h2 {
margin: 0;
color: var(--wxex-text-primary);
font: 700 18px/24px var(--wxex-font);
}
.report-delete-confirm-card p {
margin: 0;
color: var(--wxex-text-secondary);
font: 13px/20px var(--wxex-font);
}
.report-delete-confirm-card > div {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.report-delete-confirm-card button {
min-height: 32px;
padding: 0 14px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-primary);
cursor: pointer;
font: 600 13px/18px var(--wxex-font);
}
.report-delete-confirm-card button.danger {
border-color: #a64242;
background: #a64242;
color: #fff;
}
.report-delete-error {
color: #a64242 !important;
}
.report-viewer { .report-viewer {
display: flex; display: flex;
min-width: 0; min-width: 0;
@@ -3800,12 +3887,33 @@ body {
align-self: center; align-self: center;
justify-self: center; justify-self: center;
max-width: 360px; max-width: 360px;
gap: 10px; gap: 12px;
margin: auto; margin: auto;
color: var(--wxex-text-secondary); color: var(--wxex-text-secondary);
text-align: center; text-align: center;
} }
.report-center-empty-icon {
display: grid;
width: 54px;
height: 54px;
place-items: center;
justify-self: center;
border: 1px solid rgba(104, 110, 220, 0.18);
border-radius: 18px;
background: var(--wxex-ai-soft);
color: var(--wxex-ai);
}
.report-center-empty-icon svg {
width: 28px;
height: 28px;
}
.report-center-empty-icon path {
fill: currentColor;
}
.report-center-empty h2 { .report-center-empty h2 {
margin: 0; margin: 0;
color: var(--wxex-text-primary); color: var(--wxex-text-primary);
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
interface ReportEmptyStateProps { interface ReportEmptyStateProps {
icon?: 'spark'
title: string title: string
message: string message: string
actionLabel: string actionLabel: string
@@ -8,6 +9,7 @@ interface ReportEmptyStateProps {
} }
export function ReportEmptyState({ export function ReportEmptyState({
icon,
title, title,
message, message,
actionLabel, actionLabel,
@@ -15,6 +17,14 @@ export function ReportEmptyState({
}: ReportEmptyStateProps): React.ReactElement { }: ReportEmptyStateProps): React.ReactElement {
return ( return (
<div className="report-center-empty"> <div className="report-center-empty">
{icon === 'spark' && (
<div className="report-center-empty-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M12 3l1.6 5.2L19 10l-5.4 1.8L12 17l-1.6-5.2L5 10l5.4-1.8L12 3Z" />
<path d="M19 15l.8 2.3L22 18l-2.2.7L19 21l-.8-2.3L16 18l2.2-.7L19 15Z" />
</svg>
</div>
)}
<h2>{title}</h2> <h2>{title}</h2>
<p>{message}</p> <p>{message}</p>
<button type="button" onClick={onAction}> <button type="button" onClick={onAction}>
@@ -16,10 +16,14 @@ interface ReportHistorySidebarProps {
dbReady: boolean dbReady: boolean
onSelectReport: (reportId: string) => void onSelectReport: (reportId: string) => void
onCreateReport: () => void onCreateReport: () => void
onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }>
onOpenSettings: () => void onOpenSettings: () => void
} }
type HistoryFilter = 'today' | 'yesterday' | 'week' | 'older' interface ReportGroup {
label: string
reports: GeneratedReportRecord[]
}
const DAY_MS = 86400000 const DAY_MS = 86400000
@@ -31,7 +35,44 @@ const dateKey = (value: string): number => {
const formatGeneratedAt = (value: string): string => { const formatGeneratedAt = (value: string): string => {
const date = new Date(value) const date = new Date(value)
if (!Number.isFinite(date.getTime())) return value if (!Number.isFinite(date.getTime())) return value
return date.toLocaleString('zh-CN', { hour12: false }) return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
const groupLabelFor = (value: string): string => {
const date = new Date(value)
if (!Number.isFinite(date.getTime())) return '更早'
const now = new Date()
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
const startOfYesterday = startOfToday - DAY_MS
const time = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
if (time >= startOfToday) return '今天'
if (time >= startOfYesterday) return '昨天'
if (date.getFullYear() === now.getFullYear()) return `${date.getMonth() + 1}${date.getDate()}`
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`
}
const buildGroups = (reports: GeneratedReportRecord[]): ReportGroup[] => {
const groups: ReportGroup[] = []
const groupMap = new Map<string, GeneratedReportRecord[]>()
reports.forEach((report) => {
const label = groupLabelFor(report.generatedAt)
const items = groupMap.get(label) || []
items.push(report)
groupMap.set(label, items)
})
groupMap.forEach((items, label) => groups.push({ label, reports: items }))
return groups
} }
export function ReportHistorySidebar({ export function ReportHistorySidebar({
@@ -41,37 +82,44 @@ export function ReportHistorySidebar({
dbReady, dbReady,
onSelectReport, onSelectReport,
onCreateReport, onCreateReport,
onDeleteReport,
onOpenSettings onOpenSettings
}: ReportHistorySidebarProps): React.ReactElement { }: ReportHistorySidebarProps): React.ReactElement {
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
const [filter, setFilter] = useState<HistoryFilter>('today') const [pendingDelete, setPendingDelete] = useState<GeneratedReportRecord | null>(null)
const [deleteError, setDeleteError] = useState('')
const filteredReports = useMemo(() => { const reportGroups = useMemo(() => {
const lower = keyword.trim().toLowerCase() const lower = keyword.trim().toLowerCase()
const now = new Date() const filteredReports = reports
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
const startOfYesterday = startOfToday - DAY_MS
const startOfWeek = startOfToday - 6 * DAY_MS
return reports
.filter((report) => { .filter((report) => {
const haystack = `${report.contactName} ${report.dateRange}`.toLowerCase() const haystack = `${report.contactName} ${report.dateRange} ${formatGeneratedAt(
if (lower && !haystack.includes(lower)) return false report.generatedAt
const time = dateKey(report.generatedAt) )}`.toLowerCase()
if (filter === 'today') return time >= startOfToday return lower ? haystack.includes(lower) : true
if (filter === 'yesterday') return time >= startOfYesterday && time < startOfToday
if (filter === 'week') return time >= startOfWeek
return time < startOfWeek
}) })
.sort((left, right) => dateKey(right.generatedAt) - dateKey(left.generatedAt)) .sort((left, right) => dateKey(right.generatedAt) - dateKey(left.generatedAt))
}, [filter, keyword, reports])
return buildGroups(filteredReports)
}, [keyword, reports])
const confirmDelete = async (): Promise<void> => {
if (!pendingDelete) return
setDeleteError('')
const result = await onDeleteReport(pendingDelete.id)
if (!result.success) {
setDeleteError(result.error || '删除日报失败')
return
}
setPendingDelete(null)
}
return ( return (
<aside className="report-history-sidebar"> <aside className="report-history-sidebar">
<div className="report-history-header"> <div className="report-history-header">
<div> <div>
<h2>AI </h2> <h2>AI </h2>
<p></p> <p></p>
</div> </div>
<span>{reports.length}</span> <span>{reports.length}</span>
</div> </div>
@@ -86,64 +134,93 @@ export function ReportHistorySidebar({
placeholder="搜索群聊或日报" placeholder="搜索群聊或日报"
/> />
</label> </label>
<div className="report-history-filters"> <button type="button" className="report-history-create" onClick={onCreateReport}>
{[ +
['today', '今天'],
['yesterday', '昨天'],
['week', '本周'],
['older', '更早']
].map(([value, label]) => (
<button
key={value}
type="button"
className={filter === value ? 'active' : ''}
onClick={() => setFilter(value as HistoryFilter)}
>
{label}
</button> </button>
))} <div className="report-history-list" aria-label="历史报告">
</div> <div className="report-history-list-title"></div>
<div className="report-history-list"> {reportGroups.length ? (
{filteredReports.length ? ( reportGroups.map((group) => (
filteredReports.map((report) => ( <section className="report-history-group" key={group.label}>
<button <h3>{group.label}</h3>
{group.reports.map((report) => (
<div
key={report.id} key={report.id}
type="button" role="button"
tabIndex={0}
className={`report-history-item ${report.id === selectedReportId ? 'active' : ''}`} className={`report-history-item ${report.id === selectedReportId ? 'active' : ''}`}
onClick={() => onSelectReport(report.id)} onClick={() => onSelectReport(report.id)}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
onSelectReport(report.id)
}}
> >
<span className="report-history-avatar"> <span className="report-history-avatar">
{report.contactAvatar ? ( {report.contactAvatar ? (
<img src={report.contactAvatar} alt={report.contactName} referrerPolicy="no-referrer" /> <img
src={report.contactAvatar}
alt={report.contactName}
referrerPolicy="no-referrer"
/>
) : ( ) : (
report.contactName.charAt(0) report.contactName.charAt(0)
)} )}
</span> </span>
<span className="report-history-text"> <span className="report-history-text">
<b>{report.contactName}</b> <b>{report.contactName}</b>
<small> <small>{formatGeneratedAt(report.generatedAt)}</small>
{report.dateRange} · {formatGeneratedAt(report.generatedAt)}
</small>
<small>{report.messageCount} </small> <small>{report.messageCount} </small>
<em> <em>PNG{report.pngStatus === 'ready' ? '已保存' : '缺失'}</em>
HTML {report.htmlStatus === 'ready' ? '已保存' : '缺失'} · PNG{' '}
{report.pngStatus === 'ready' ? '已保存' : '缺失'}
</em>
</span> </span>
<button
type="button"
className="report-history-delete"
title="删除日报"
onClick={(event) => {
event.stopPropagation()
setPendingDelete(report)
}}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
event.stopPropagation()
setPendingDelete(report)
}}
>
</button> </button>
</div>
))}
</section>
)) ))
) : ( ) : (
<div className="report-history-empty"> <div className="report-history-empty">
<b></b> <b></b>
<button type="button" onClick={onCreateReport}> <span></span>
</button>
</div> </div>
)} )}
</div> </div>
<div className="report-history-account"> <div className="report-history-account">
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} /> <AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} />
</div> </div>
{pendingDelete && (
<div className="report-delete-confirm" role="dialog" aria-modal="true">
<div className="report-delete-confirm-card">
<h2></h2>
<p></p>
{deleteError && <p className="report-delete-error">{deleteError}</p>}
<div>
<button type="button" onClick={() => setPendingDelete(null)}>
</button>
<button type="button" className="danger" onClick={() => void confirmDelete()}>
</button>
</div>
</div>
</div>
)}
</aside> </aside>
) )
} }
@@ -36,7 +36,7 @@ export function ReportSettingsPanel({
<aside className="report-settings-panel"> <aside className="report-settings-panel">
<header> <header>
<h2></h2> <h2></h2>
<p></p> <p></p>
</header> </header>
<section className="report-settings-section"> <section className="report-settings-section">
<h3></h3> <h3></h3>
@@ -75,8 +75,8 @@ export function ReportSettingsPanel({
)} )}
</section> </section>
<section className="report-settings-section muted"> <section className="report-settings-section muted">
<h3></h3> <h3></h3>
<p></p> <p></p>
</section> </section>
</aside> </aside>
) )
@@ -6,6 +6,7 @@ import { ReportZoomBar } from './ReportZoomBar'
interface ReportViewerProps { interface ReportViewerProps {
report: GeneratedReportRecord | null report: GeneratedReportRecord | null
hasReports: boolean
onBackToConfigure: () => void onBackToConfigure: () => void
onRegenerate: () => void onRegenerate: () => void
onCopyImage: (report: GeneratedReportRecord) => Promise<{ success: boolean; error?: string }> onCopyImage: (report: GeneratedReportRecord) => Promise<{ success: boolean; error?: string }>
@@ -15,6 +16,7 @@ interface ReportViewerProps {
export function ReportViewer({ export function ReportViewer({
report, report,
hasReports,
onBackToConfigure, onBackToConfigure,
onRegenerate, onRegenerate,
onCopyImage, onCopyImage,
@@ -39,7 +41,7 @@ export function ReportViewer({
}, [onImageSizeChange, report?.id]) }, [onImageSizeChange, report?.id])
const title = useMemo( const title = useMemo(
() => (report ? `${report.contactName} 群聊日报` : 'AI 群聊日报'), () => (report ? `${report.contactName} 群聊日报` : 'AI 日报'),
[report] [report]
) )
@@ -66,9 +68,14 @@ export function ReportViewer({
return ( return (
<main className="report-viewer"> <main className="report-viewer">
<ReportEmptyState <ReportEmptyState
title="尚未生成日报" icon="spark"
message="生成一份 AI 群聊日报后,可以在这里查看本地保存的长图。" title={hasReports ? '选择一份历史日报' : 'AI日报'}
actionLabel="生成新日报" message={
hasReports
? '从左侧历史报告中选择一份日报,查看本地保存的长图和文件信息。'
: '还没有生成过日报。选择一个群聊,让 AI 自动整理讨论重点、热点话题和重要消息。'
}
actionLabel="开始生成日报"
onAction={onBackToConfigure} onAction={onBackToConfigure}
/> />
</main> </main>
+6
View File
@@ -40,3 +40,9 @@ export interface SaveGeneratedReportResult {
record?: GeneratedReportRecord record?: GeneratedReportRecord
error?: string error?: string
} }
export interface DeleteGeneratedReportResult {
success: boolean
deletedId?: string
error?: string
}