feat: 히스토리 자동 타이틀 + 카드 UI 분리

전사 완료 후 LLM이 10단어 이내 제목 자동 생성 (fire-and-forget).
DB에 title 컬럼 추가 + 마이그레이션.
HistoryEntryCard: 제목(볼드, ellipsis) + 내용(2줄 미리보기) 분리.
This commit is contained in:
Yun Chan 2026-04-08 11:54:37 +09:00
parent b452ebbe52
commit 239fb820b8
5 changed files with 68 additions and 1 deletions

View file

@ -36,6 +36,10 @@ class HistoryService {
this._updateStats(input.duration, input.wordCount)
logger.info(`History entry created: ${id}`)
// 비동기로 LLM 타이틀 자동 생성 (fire-and-forget)
this.generateTitle(id).catch(() => { /* ignore */ })
return this._toEntry(entry as History)
}
@ -182,9 +186,42 @@ class HistoryService {
.run()
}
/** 전사 완료 후 LLM으로 자동 타이틀 생성 */
async generateTitle(id: string): Promise<string | null> {
const entry = this.getById(id)
if (!entry) return null
const text = entry.polishedText || entry.originalText
if (!text || text.length < 10) return null
try {
const { getLocalLLMService } = await import('./LocalLLMService')
const llm = getLocalLLMService()
const result = await llm.generate(
text.slice(0, 2000),
{
systemPrompt: '다음 텍스트의 핵심 내용을 10단어 이내의 짧은 제목으로 만들어주세요. 제목만 출력하세요. 따옴표나 마침표를 붙이지 마세요.',
temperature: 0.3,
maxTokens: 50,
},
)
const title = result.text.trim().replace(/^["']|["']$/g, '').slice(0, 80)
if (title) {
const db = getDatabase()
db.update(history).set({ title, updatedAt: Date.now() }).where(eq(history.id, id)).run()
logger.info(`Auto title generated: ${id} → "${title}"`)
return title
}
} catch (err) {
logger.warn(`Auto title generation failed: ${err instanceof Error ? err.message : String(err)}`)
}
return null
}
private _toEntry(row: History): HistoryEntry {
return {
id: row.id,
title: row.title ?? null,
originalText: row.originalText,
polishedText: row.polishedText,
focusedApp: row.focusedApp,
@ -204,7 +241,8 @@ class HistoryService {
llmLatencyMs: row.llmLatencyMs,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
appVersion: row.appVersion
appVersion: row.appVersion,
summaryText: row.summaryText ?? null,
}
}
}