feat(app-expo): show memoir chapter draft progress on list cards
Explain invite vs. generating state using MemoirState slots, surface remaining characters to the display gate, refresh memoir-state on pull, and sync i18n. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
53
app-expo/src/features/memoir/draft-progress.ts
Normal file
53
app-expo/src/features/memoir/draft-progress.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { MemoirState } from './types';
|
||||
|
||||
import { CHAPTER_CATEGORY_BY_ORDER_INDEX } from './framework-chapter-keys';
|
||||
|
||||
/** 与后端 `reading_segment_materialize.MIN_STORY_CHARS_IN_CHAPTER` 一致:章节可读成稿阈值 */
|
||||
export const MIN_CHAPTER_DISPLAY_CHARS = 300;
|
||||
|
||||
/**
|
||||
* Chat 口述槽的阶段键(MemoirState.slots);多章 career_* 共用 `career`,beliefs/summary 与 `belief` 槽对齐。
|
||||
*/
|
||||
export function chapterCategoryToInterviewStage(category: string): string {
|
||||
const c = (category ?? '').trim();
|
||||
if (!c) return 'childhood';
|
||||
if (c.startsWith('career_')) return 'career';
|
||||
if (c === 'beliefs' || c === 'summary') return 'belief';
|
||||
return c;
|
||||
}
|
||||
|
||||
export function resolvedChapterCategory(vm: {
|
||||
category: string;
|
||||
orderIndex: number;
|
||||
}): string {
|
||||
const raw = vm.category?.trim();
|
||||
if (raw) return raw;
|
||||
return CHAPTER_CATEGORY_BY_ORDER_INDEX[vm.orderIndex] ?? 'childhood';
|
||||
}
|
||||
|
||||
export function interviewStageHasSnippetMaterial(
|
||||
slots: MemoirState['slots'] | undefined,
|
||||
stage: string,
|
||||
): boolean {
|
||||
if (!slots) return false;
|
||||
const block = slots[stage];
|
||||
if (!block) return false;
|
||||
return Object.values(block).some(
|
||||
(cell) => (cell?.snippet ?? '').trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
export function memoirDraftHasStarted(
|
||||
slots: MemoirState['slots'] | undefined,
|
||||
chapterCategory: string,
|
||||
chapterWordCount: number,
|
||||
): boolean {
|
||||
const stage = chapterCategoryToInterviewStage(chapterCategory);
|
||||
if (chapterWordCount > 0) return true;
|
||||
return interviewStageHasSnippetMaterial(slots, stage);
|
||||
}
|
||||
|
||||
export function memoirDraftCharsRemaining(chapterWordCount: number): number {
|
||||
const n = typeof chapterWordCount === 'number' ? chapterWordCount : 0;
|
||||
return Math.max(0, MIN_CHAPTER_DISPLAY_CHARS - n);
|
||||
}
|
||||
@@ -16,6 +16,18 @@ export const FRAMEWORK_CHAPTER_KEYS = [
|
||||
|
||||
export type FrameworkChapterKey = (typeof FRAMEWORK_CHAPTER_KEYS)[number];
|
||||
|
||||
/** 与后端 `CHAPTER_ORDER` 一致(用于占位章节缺省 category) */
|
||||
export const CHAPTER_CATEGORY_BY_ORDER_INDEX = [
|
||||
'childhood',
|
||||
'education',
|
||||
'career_early',
|
||||
'career_achievement',
|
||||
'career_challenge',
|
||||
'family',
|
||||
'beliefs',
|
||||
'summary',
|
||||
] as const;
|
||||
|
||||
export function buildFrameworkChapterPlaceholders(
|
||||
tr: (key: string) => string,
|
||||
): ChapterViewModel[] {
|
||||
|
||||
Reference in New Issue
Block a user