This commit is contained in:
xuncha
2026-02-01 01:26:43 +08:00
parent 5413d7e2c8
commit f40f885af3
4 changed files with 302 additions and 371 deletions

View File

@@ -16,7 +16,7 @@ export interface DualReportFirstChat {
senderUsername?: string senderUsername?: string
} }
export interface DualReportYearlyStats { export interface DualReportStats {
totalMessages: number totalMessages: number
totalWords: number totalWords: number
imageCount: number imageCount: number
@@ -28,19 +28,13 @@ export interface DualReportYearlyStats {
friendTopEmojiUrl?: string friendTopEmojiUrl?: string
} }
export interface DualReportWordCloud {
words: Array<{ phrase: string; count: number }>
totalWords: number
totalMessages: number
}
export interface DualReportData { export interface DualReportData {
year: number year: number
myName: string selfName: string
friendUsername: string friendUsername: string
friendName: string friendName: string
firstChat: DualReportFirstChat | null firstChat: DualReportFirstChat | null
thisYearFirstChat?: { yearFirstChat?: {
createTime: number createTime: number
createTimeStr: string createTimeStr: string
content: string content: string
@@ -48,8 +42,8 @@ export interface DualReportData {
friendName: string friendName: string
firstThreeMessages: DualReportMessage[] firstThreeMessages: DualReportMessage[]
} | null } | null
yearlyStats: DualReportYearlyStats stats: DualReportStats
wordCloud: DualReportWordCloud topPhrases: Array<{ phrase: string; count: number }>
} }
class DualReportService { class DualReportService {
@@ -272,7 +266,7 @@ class DualReportService {
} }
} }
let thisYearFirstChat: DualReportData['thisYearFirstChat'] = null let yearFirstChat: DualReportData['yearFirstChat'] = null
if (!isAllTime) { if (!isAllTime) {
this.reportProgress('获取今年首次聊天...', 20, onProgress) this.reportProgress('获取今年首次聊天...', 20, onProgress)
const firstYearRows = await this.getFirstMessages(friendUsername, 3, startTime, endTime) const firstYearRows = await this.getFirstMessages(friendUsername, 3, startTime, endTime)
@@ -289,7 +283,7 @@ class DualReportService {
createTimeStr: this.formatDateTime(msgTime) createTimeStr: this.formatDateTime(msgTime)
} }
}) })
thisYearFirstChat = { yearFirstChat = {
createTime, createTime,
createTimeStr: this.formatDateTime(createTime), createTimeStr: this.formatDateTime(createTime),
content: String(this.decodeMessageContent(firstRow.message_content, firstRow.compress_content) || ''), content: String(this.decodeMessageContent(firstRow.message_content, firstRow.compress_content) || ''),
@@ -301,7 +295,7 @@ class DualReportService {
} }
this.reportProgress('统计聊天数据...', 30, onProgress) this.reportProgress('统计聊天数据...', 30, onProgress)
const yearlyStats: DualReportYearlyStats = { const stats: DualReportStats = {
totalMessages: 0, totalMessages: 0,
totalWords: 0, totalWords: 0,
imageCount: 0, imageCount: 0,
@@ -334,12 +328,12 @@ class DualReportService {
for (const row of batch.rows) { for (const row of batch.rows) {
const localType = parseInt(row.local_type || row.type || '1', 10) const localType = parseInt(row.local_type || row.type || '1', 10)
const isSent = this.resolveIsSent(row, rawWxid, cleanedWxid) const isSent = this.resolveIsSent(row, rawWxid, cleanedWxid)
yearlyStats.totalMessages += 1 stats.totalMessages += 1
if (localType === 3) yearlyStats.imageCount += 1 if (localType === 3) stats.imageCount += 1
if (localType === 34) yearlyStats.voiceCount += 1 if (localType === 34) stats.voiceCount += 1
if (localType === 47) { if (localType === 47) {
yearlyStats.emojiCount += 1 stats.emojiCount += 1
const content = this.decodeMessageContent(row.message_content, row.compress_content) const content = this.decodeMessageContent(row.message_content, row.compress_content)
const md5 = this.extractEmojiMd5(content) const md5 = this.extractEmojiMd5(content)
const url = this.extractEmojiUrl(content) const url = this.extractEmojiUrl(content)
@@ -357,7 +351,7 @@ class DualReportService {
const content = this.decodeMessageContent(row.message_content, row.compress_content) const content = this.decodeMessageContent(row.message_content, row.compress_content)
const text = String(content || '').trim() const text = String(content || '').trim()
if (text.length > 0) { if (text.length > 0) {
yearlyStats.totalWords += text.replace(/\s+/g, '').length stats.totalWords += text.replace(/\s+/g, '').length
const normalized = text.replace(/\s+/g, ' ').trim() const normalized = text.replace(/\s+/g, ' ').trim()
if (normalized.length >= 2 && if (normalized.length >= 2 &&
normalized.length <= 50 && normalized.length <= 50 &&
@@ -405,33 +399,27 @@ class DualReportService {
const myTopEmojiMd5 = pickTop(myEmojiCounts) const myTopEmojiMd5 = pickTop(myEmojiCounts)
const friendTopEmojiMd5 = pickTop(friendEmojiCounts) const friendTopEmojiMd5 = pickTop(friendEmojiCounts)
yearlyStats.myTopEmojiMd5 = myTopEmojiMd5 stats.myTopEmojiMd5 = myTopEmojiMd5
yearlyStats.friendTopEmojiMd5 = friendTopEmojiMd5 stats.friendTopEmojiMd5 = friendTopEmojiMd5
yearlyStats.myTopEmojiUrl = myTopEmojiMd5 ? myEmojiUrlMap.get(myTopEmojiMd5) : undefined stats.myTopEmojiUrl = myTopEmojiMd5 ? myEmojiUrlMap.get(myTopEmojiMd5) : undefined
yearlyStats.friendTopEmojiUrl = friendTopEmojiMd5 ? friendEmojiUrlMap.get(friendTopEmojiMd5) : undefined stats.friendTopEmojiUrl = friendTopEmojiMd5 ? friendEmojiUrlMap.get(friendTopEmojiMd5) : undefined
this.reportProgress('生成常用语词云...', 85, onProgress) this.reportProgress('生成常用语词云...', 85, onProgress)
const wordCloudWords = Array.from(wordCountMap.entries()) const topPhrases = Array.from(wordCountMap.entries())
.filter(([_, count]) => count >= 2) .filter(([_, count]) => count >= 2)
.sort((a, b) => b[1] - a[1]) .sort((a, b) => b[1] - a[1])
.slice(0, 50) .slice(0, 50)
.map(([phrase, count]) => ({ phrase, count })) .map(([phrase, count]) => ({ phrase, count }))
const wordCloud: DualReportWordCloud = {
words: wordCloudWords,
totalWords: yearlyStats.totalWords,
totalMessages: yearlyStats.totalMessages
}
const reportData: DualReportData = { const reportData: DualReportData = {
year: reportYear, year: reportYear,
myName, selfName: myName,
friendUsername, friendUsername,
friendName, friendName,
firstChat, firstChat,
thisYearFirstChat, yearFirstChat,
yearlyStats, stats,
wordCloud topPhrases
} }
this.reportProgress('双人报告生成完成', 100, onProgress) this.reportProgress('双人报告生成完成', 100, onProgress)

View File

@@ -1,220 +1,130 @@
.dual-report-window { .annual-report-window.dual-report-window {
color: var(--text-primary); .dual-names {
padding: 32px 24px 60px; font-size: clamp(24px, 4vw, 40px);
background: var(--bg-primary); font-weight: 700;
}
.dual-report-window.loading,
.dual-report-window.error {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
min-height: 60vh;
gap: 12px; gap: 12px;
color: var(--text-tertiary); margin: 8px 0 16px;
} color: var(--ar-text-main);
.dual-section { .amp {
background: var(--card-bg); color: var(--ar-primary);
border: 1px solid var(--border-color); }
border-radius: 20px;
padding: 24px;
margin: 16px auto;
max-width: 900px;
}
.dual-section.cover {
text-align: center;
background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 10%, transparent) 0%, var(--card-bg) 100%);
.label {
font-size: 12px;
letter-spacing: 2px;
color: var(--text-tertiary);
margin-bottom: 12px;
} }
h1 { .dual-info-grid {
margin: 0 0 12px; display: grid;
font-size: 36px; grid-template-columns: repeat(2, minmax(0, 1fr));
}
p {
margin: 0;
color: var(--text-secondary);
}
}
.section-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
.info-card {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
gap: 16px; gap: 16px;
font-size: 14px; margin-top: 16px;
} }
.info-label { .dual-info-card {
color: var(--text-tertiary); background: var(--ar-card-bg);
} border: 1px solid var(--bg-tertiary, rgba(0, 0, 0, 0.05));
border-radius: 14px;
padding: 16px;
.info-value { &.full {
color: var(--text-primary); grid-column: 1 / -1;
}
.info-label {
font-size: 12px;
color: var(--ar-text-sub);
margin-bottom: 8px;
}
.info-value {
font-size: 16px;
font-weight: 600; font-weight: 600;
} color: var(--ar-text-main);
}
}
.info-empty { .dual-message-list {
color: var(--text-tertiary); margin-top: 16px;
}
.message-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px;
margin-top: 8px;
}
.message-item {
padding: 10px 12px;
border-radius: 12px;
background: color-mix(in srgb, var(--primary) 6%, transparent);
&.received {
background: color-mix(in srgb, var(--border-color) 35%, transparent);
}
}
.message-meta {
font-size: 12px;
color: var(--text-tertiary);
margin-bottom: 6px;
}
.message-content {
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px; gap: 12px;
margin-bottom: 16px;
}
.stat-card {
background: color-mix(in srgb, var(--primary) 6%, transparent);
border-radius: 12px;
padding: 14px;
text-align: center;
.stat-value {
font-size: 20px;
font-weight: 700;
} }
.stat-label { .dual-message {
font-size: 12px; background: var(--ar-card-bg);
color: var(--text-tertiary);
margin-top: 4px;
}
}
.emoji-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.emoji-card {
border: 1px solid var(--border-color);
border-radius: 14px; border-radius: 14px;
padding: 14px; padding: 14px;
&.received {
background: var(--ar-card-bg-hover);
}
.message-meta {
font-size: 12px;
color: var(--ar-text-sub);
margin-bottom: 6px;
}
.message-content {
font-size: 14px;
color: var(--ar-text-main);
}
}
.dual-stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin: 20px 0 24px;
}
.dual-stat-card {
background: var(--ar-card-bg);
border-radius: 16px;
padding: 18px;
text-align: center;
}
.emoji-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.emoji-card {
border: 1px solid var(--bg-tertiary, rgba(0, 0, 0, 0.08));
border-radius: 16px;
padding: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--ar-card-bg);
img { img {
width: 64px; width: 64px;
height: 64px; height: 64px;
object-fit: contain; object-fit: contain;
} }
} }
.emoji-title { .emoji-title {
font-size: 12px; font-size: 12px;
color: var(--text-tertiary); color: var(--ar-text-sub);
} }
.emoji-placeholder { .emoji-placeholder {
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--ar-text-sub);
word-break: break-all; word-break: break-all;
text-align: center; text-align: center;
} }
.word-cloud-wrapper { .word-cloud-empty {
position: relative; color: var(--ar-text-sub);
width: 100%;
padding-top: 80%;
background: color-mix(in srgb, var(--primary) 4%, transparent);
border-radius: 18px;
overflow: hidden;
}
.word-cloud-inner {
position: absolute;
inset: 0;
}
.word-tag {
position: absolute;
font-weight: 600;
color: var(--text-primary);
transform: translate(-50%, -50%);
opacity: 0;
animation: fadeUp 0.8s ease forwards;
}
.word-cloud-empty {
color: var(--text-tertiary);
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
padding: 40px 0; padding: 24px 0;
} }
.progress {
font-size: 20px;
font-weight: 700;
}
.stage {
font-size: 12px;
color: var(--text-tertiary);
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeUp {
from { opacity: 0; transform: translate(-50%, -50%) translateY(10px); }
to { opacity: var(--final-opacity, 1); transform: translate(-50%, -50%) translateY(0); }
} }

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, type CSSProperties } from 'react' import { useEffect, useState, type CSSProperties } from 'react'
import { Loader2 } from 'lucide-react' import './AnnualReportWindow.scss'
import './DualReportWindow.scss' import './DualReportWindow.scss'
interface DualReportMessage { interface DualReportMessage {
@@ -11,7 +11,7 @@ interface DualReportMessage {
interface DualReportData { interface DualReportData {
year: number year: number
myName: string selfName: string
friendUsername: string friendUsername: string
friendName: string friendName: string
firstChat: { firstChat: {
@@ -21,7 +21,7 @@ interface DualReportData {
isSentByMe: boolean isSentByMe: boolean
senderUsername?: string senderUsername?: string
} | null } | null
thisYearFirstChat?: { yearFirstChat?: {
createTime: number createTime: number
createTimeStr: string createTimeStr: string
content: string content: string
@@ -29,7 +29,7 @@ interface DualReportData {
friendName: string friendName: string
firstThreeMessages: DualReportMessage[] firstThreeMessages: DualReportMessage[]
} | null } | null
yearlyStats: { stats: {
totalMessages: number totalMessages: number
totalWords: number totalWords: number
imageCount: number imageCount: number
@@ -40,19 +40,16 @@ interface DualReportData {
myTopEmojiUrl?: string myTopEmojiUrl?: string
friendTopEmojiUrl?: string friendTopEmojiUrl?: string
} }
wordCloud: { topPhrases: Array<{ phrase: string; count: number }>
words: Array<{ phrase: string; count: number }>
totalWords: number
totalMessages: number
}
} }
const WordCloud = ({ words }: { words: { phrase: string; count: number }[] }) => { const WordCloud = ({ words }: { words: { phrase: string; count: number }[] }) => {
if (!words || words.length === 0) { if (!words || words.length === 0) {
return <div className="word-cloud-empty"></div> return <div className="word-cloud-empty"></div>
} }
const maxCount = words.length > 0 ? words[0].count : 1 const sortedWords = [...words].sort((a, b) => b.count - a.count)
const topWords = words.slice(0, 32) const maxCount = sortedWords.length > 0 ? sortedWords[0].count : 1
const topWords = sortedWords.slice(0, 32)
const baseSize = 520 const baseSize = 520
const seededRandom = (seed: number) => { const seededRandom = (seed: number) => {
@@ -205,7 +202,7 @@ function DualReportWindow() {
useEffect(() => { useEffect(() => {
const loadEmojis = async () => { const loadEmojis = async () => {
if (!reportData) return if (!reportData) return
const stats = reportData.yearlyStats const stats = reportData.stats
if (stats.myTopEmojiUrl) { if (stats.myTopEmojiUrl) {
const res = await window.electronAPI.chat.downloadEmoji(stats.myTopEmojiUrl, stats.myTopEmojiMd5) const res = await window.electronAPI.chat.downloadEmoji(stats.myTopEmojiUrl, stats.myTopEmojiMd5)
if (res.success && res.localPath) { if (res.success && res.localPath) {
@@ -224,25 +221,35 @@ function DualReportWindow() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="dual-report-window loading"> <div className="annual-report-window loading">
<Loader2 size={36} className="spin" /> <div className="loading-ring">
<div className="progress">{loadingProgress}%</div> <svg viewBox="0 0 100 100">
<div className="stage">{loadingStage}</div> <circle className="ring-bg" cx="50" cy="50" r="42" />
<circle
className="ring-progress"
cx="50" cy="50" r="42"
style={{ strokeDashoffset: 264 - (264 * loadingProgress / 100) }}
/>
</svg>
<span className="ring-text">{loadingProgress}%</span>
</div>
<p className="loading-stage">{loadingStage}</p>
<p className="loading-hint"></p>
</div> </div>
) )
} }
if (error) { if (error) {
return ( return (
<div className="dual-report-window error"> <div className="annual-report-window error">
<p>{error}</p> <p>: {error}</p>
</div> </div>
) )
} }
if (!reportData) { if (!reportData) {
return ( return (
<div className="dual-report-window error"> <div className="annual-report-window error">
<p></p> <p></p>
</div> </div>
) )
@@ -253,90 +260,112 @@ function DualReportWindow() {
const daysSince = firstChat const daysSince = firstChat
? Math.max(0, Math.floor((Date.now() - firstChat.createTime) / 86400000)) ? Math.max(0, Math.floor((Date.now() - firstChat.createTime) / 86400000))
: null : null
const thisYearFirstChat = reportData.thisYearFirstChat const yearFirstChat = reportData.yearFirstChat
const stats = reportData.yearlyStats const stats = reportData.stats
return ( return (
<div className="dual-report-window"> <div className="annual-report-window dual-report-window">
<section className="dual-section cover"> <div className="drag-region" />
<div className="label">DUAL REPORT</div>
<h1>{reportData.myName} &amp; {reportData.friendName}</h1> <div className="bg-decoration">
<p></p> <div className="deco-circle c1" />
<div className="deco-circle c2" />
<div className="deco-circle c3" />
<div className="deco-circle c4" />
<div className="deco-circle c5" />
</div>
<div className="report-scroll-view">
<div className="report-container">
<section className="section">
<div className="label-text">WEFLOW · DUAL REPORT</div>
<h1 className="hero-title">{yearTitle}<br /></h1>
<hr className="divider" />
<div className="dual-names">
<span>{reportData.selfName}</span>
<span className="amp">&amp;</span>
<span>{reportData.friendName}</span>
</div>
<p className="hero-desc"></p>
</section> </section>
<section className="dual-section"> <section className="section">
<div className="section-title"></div> <div className="label-text"></div>
<h2 className="hero-title"></h2>
{firstChat ? ( {firstChat ? (
<div className="info-card"> <div className="dual-info-grid">
<div className="info-row"> <div className="dual-info-card">
<span className="info-label"></span> <div className="info-label"></div>
<span className="info-value">{firstChat.createTimeStr}</span> <div className="info-value">{firstChat.createTimeStr}</div>
</div> </div>
<div className="info-row"> <div className="dual-info-card">
<span className="info-label"></span> <div className="info-label"></div>
<span className="info-value">{daysSince} </span> <div className="info-value">{daysSince} </div>
</div> </div>
<div className="info-row"> <div className="dual-info-card full">
<span className="info-label"></span> <div className="info-label"></div>
<span className="info-value">{firstChat.content || '(空)'}</span> <div className="info-value">{firstChat.content || '(空)'}</div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="info-empty"></div> <p className="hero-desc"></p>
)} )}
</section> </section>
{thisYearFirstChat ? ( {yearFirstChat ? (
<section className="dual-section"> <section className="section">
<div className="section-title"></div> <div className="label-text"></div>
<div className="info-card"> <h2 className="hero-title"></h2>
<div className="info-row"> <div className="dual-info-grid">
<span className="info-label"></span> <div className="dual-info-card">
<span className="info-value">{thisYearFirstChat.createTimeStr}</span> <div className="info-label"></div>
<div className="info-value">{yearFirstChat.createTimeStr}</div>
</div> </div>
<div className="info-row"> <div className="dual-info-card">
<span className="info-label"></span> <div className="info-label"></div>
<span className="info-value">{thisYearFirstChat.isSentByMe ? reportData.myName : reportData.friendName}</span> <div className="info-value">{yearFirstChat.isSentByMe ? reportData.selfName : reportData.friendName}</div>
</div> </div>
<div className="message-list"> </div>
{thisYearFirstChat.firstThreeMessages.map((msg, idx) => ( <div className="dual-message-list">
<div key={idx} className={`message-item ${msg.isSentByMe ? 'sent' : 'received'}`}> {yearFirstChat.firstThreeMessages.map((msg, idx) => (
<div className="message-meta">{msg.isSentByMe ? reportData.myName : reportData.friendName} · {msg.createTimeStr}</div> <div key={idx} className={`dual-message ${msg.isSentByMe ? 'sent' : 'received'}`}>
<div className="message-meta">{msg.isSentByMe ? reportData.selfName : reportData.friendName} · {msg.createTimeStr}</div>
<div className="message-content">{msg.content || '(空)'}</div> <div className="message-content">{msg.content || '(空)'}</div>
</div> </div>
))} ))}
</div> </div>
</div>
</section> </section>
) : null} ) : null}
<section className="dual-section"> <section className="section">
<div className="section-title">{yearTitle}</div> <div className="label-text"></div>
<WordCloud words={reportData.wordCloud.words} /> <h2 className="hero-title">{yearTitle}</h2>
<WordCloud words={reportData.topPhrases} />
</section> </section>
<section className="dual-section"> <section className="section">
<div className="section-title">{yearTitle}</div> <div className="label-text"></div>
<div className="stats-grid"> <h2 className="hero-title">{yearTitle}</h2>
<div className="stat-card"> <div className="dual-stat-grid">
<div className="stat-value">{stats.totalMessages.toLocaleString()}</div> <div className="dual-stat-card">
<div className="stat-label"></div> <div className="stat-num">{stats.totalMessages.toLocaleString()}</div>
<div className="stat-unit"></div>
</div> </div>
<div className="stat-card"> <div className="dual-stat-card">
<div className="stat-value">{stats.totalWords.toLocaleString()}</div> <div className="stat-num">{stats.totalWords.toLocaleString()}</div>
<div className="stat-label"></div> <div className="stat-unit"></div>
</div> </div>
<div className="stat-card"> <div className="dual-stat-card">
<div className="stat-value">{stats.imageCount.toLocaleString()}</div> <div className="stat-num">{stats.imageCount.toLocaleString()}</div>
<div className="stat-label"></div> <div className="stat-unit"></div>
</div> </div>
<div className="stat-card"> <div className="dual-stat-card">
<div className="stat-value">{stats.voiceCount.toLocaleString()}</div> <div className="stat-num">{stats.voiceCount.toLocaleString()}</div>
<div className="stat-label"></div> <div className="stat-unit"></div>
</div> </div>
<div className="stat-card"> <div className="dual-stat-card">
<div className="stat-value">{stats.emojiCount.toLocaleString()}</div> <div className="stat-num">{stats.emojiCount.toLocaleString()}</div>
<div className="stat-label"></div> <div className="stat-unit"></div>
</div> </div>
</div> </div>
@@ -359,6 +388,14 @@ function DualReportWindow() {
</div> </div>
</div> </div>
</section> </section>
<section className="section">
<div className="label-text"></div>
<h2 className="hero-title"></h2>
<p className="hero-desc"></p>
</section>
</div>
</div>
</div> </div>
) )
} }

View File

@@ -342,7 +342,7 @@ export interface ElectronAPI {
success: boolean success: boolean
data?: { data?: {
year: number year: number
myName: string selfName: string
friendUsername: string friendUsername: string
friendName: string friendName: string
firstChat: { firstChat: {
@@ -352,7 +352,7 @@ export interface ElectronAPI {
isSentByMe: boolean isSentByMe: boolean
senderUsername?: string senderUsername?: string
} | null } | null
thisYearFirstChat?: { yearFirstChat?: {
createTime: number createTime: number
createTimeStr: string createTimeStr: string
content: string content: string
@@ -365,7 +365,7 @@ export interface ElectronAPI {
createTimeStr: string createTimeStr: string
}> }>
} | null } | null
yearlyStats: { stats: {
totalMessages: number totalMessages: number
totalWords: number totalWords: number
imageCount: number imageCount: number
@@ -376,11 +376,7 @@ export interface ElectronAPI {
myTopEmojiUrl?: string myTopEmojiUrl?: string
friendTopEmojiUrl?: string friendTopEmojiUrl?: string
} }
wordCloud: { topPhrases: Array<{ phrase: string; count: number }>
words: Array<{ phrase: string; count: number }>
totalWords: number
totalMessages: number
}
} }
error?: string error?: string
}> }>