feat: 히스토리 자동 타이틀 + 카드 UI 분리
전사 완료 후 LLM이 10단어 이내 제목 자동 생성 (fire-and-forget). DB에 title 컬럼 추가 + 마이그레이션. HistoryEntryCard: 제목(볼드, ellipsis) + 내용(2줄 미리보기) 분리.
This commit is contained in:
parent
b452ebbe52
commit
239fb820b8
5 changed files with 68 additions and 1 deletions
|
|
@ -191,6 +191,17 @@ export function initDatabase(): BetterSQLite3Database<typeof schema> {
|
||||||
logger.warn('edited_transcript migration check failed:', err)
|
logger.warn('edited_transcript migration check failed:', err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 15: history 테이블에 title 컬럼 마이그레이션
|
||||||
|
try {
|
||||||
|
const histCols = sqlite.pragma('table_info(history)') as Array<{ name: string }>
|
||||||
|
if (!histCols.some((c) => c.name === 'title')) {
|
||||||
|
sqlite.exec('ALTER TABLE history ADD COLUMN title TEXT')
|
||||||
|
logger.info('Migrated: added title column to history')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('title migration check failed:', err)
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 12.2: history 테이블에 summary_text 컬럼 마이그레이션
|
// Phase 12.2: history 테이블에 summary_text 컬럼 마이그레이션
|
||||||
try {
|
try {
|
||||||
const columns = sqlite.pragma('table_info(history)') as Array<{ name: string }>
|
const columns = sqlite.pragma('table_info(history)') as Array<{ name: string }>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export const history = sqliteTable(
|
||||||
'history',
|
'history',
|
||||||
{
|
{
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
|
title: text('title'),
|
||||||
originalText: text('original_text').notNull(),
|
originalText: text('original_text').notNull(),
|
||||||
polishedText: text('polished_text'),
|
polishedText: text('polished_text'),
|
||||||
focusedApp: text('focused_app'),
|
focusedApp: text('focused_app'),
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ class HistoryService {
|
||||||
this._updateStats(input.duration, input.wordCount)
|
this._updateStats(input.duration, input.wordCount)
|
||||||
|
|
||||||
logger.info(`History entry created: ${id}`)
|
logger.info(`History entry created: ${id}`)
|
||||||
|
|
||||||
|
// 비동기로 LLM 타이틀 자동 생성 (fire-and-forget)
|
||||||
|
this.generateTitle(id).catch(() => { /* ignore */ })
|
||||||
|
|
||||||
return this._toEntry(entry as History)
|
return this._toEntry(entry as History)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,9 +186,42 @@ class HistoryService {
|
||||||
.run()
|
.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 {
|
private _toEntry(row: History): HistoryEntry {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
title: row.title ?? null,
|
||||||
originalText: row.originalText,
|
originalText: row.originalText,
|
||||||
polishedText: row.polishedText,
|
polishedText: row.polishedText,
|
||||||
focusedApp: row.focusedApp,
|
focusedApp: row.focusedApp,
|
||||||
|
|
@ -204,7 +241,8 @@ class HistoryService {
|
||||||
llmLatencyMs: row.llmLatencyMs,
|
llmLatencyMs: row.llmLatencyMs,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
appVersion: row.appVersion
|
appVersion: row.appVersion,
|
||||||
|
summaryText: row.summaryText ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,13 +100,29 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
<Led color={entry.status === 'completed' ? 'green' : 'red'} size={6} />
|
<Led color={entry.status === 'completed' ? 'green' : 'red'} size={6} />
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{/* 타이틀 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: d3roTypo.compact.size,
|
fontSize: d3roTypo.compact.size,
|
||||||
|
fontWeight: 600,
|
||||||
color: d3roPalette.text.primary,
|
color: d3roPalette.text.primary,
|
||||||
lineHeight: d3roTypo.compact.line,
|
lineHeight: d3roTypo.compact.line,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{entry.title ?? displayText.slice(0, 60)}
|
||||||
|
</Box>
|
||||||
|
{/* 내용 미리보기 */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontSize: d3roTypo.small.size,
|
||||||
|
color: d3roPalette.text.secondary,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,7 @@ export interface ConfigChangedEvent {
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
id: string
|
id: string
|
||||||
|
title: string | null
|
||||||
originalText: string
|
originalText: string
|
||||||
polishedText: string | null
|
polishedText: string | null
|
||||||
focusedApp: string | null
|
focusedApp: string | null
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue