重构回忆录为 story-first / markdown-first 架构并整合图片意图与前端 UI 修复
本次 squash merge 将 codex-story-first-image-intent 的整体改动合入 development,核心内容包括: 1. 后端数据与迁移:新增 stories、story_versions、story_image_intents、chapter_cover_intents、assets 等模型与 Alembic 迁移,建立 story-first、markdown-first、asset-first 的主数据链路。 2. 生成与任务链:引入 StoryBuilderOrchestrator、ChapterComposerOrchestrator、story_image_tasks、chapter_cover_tasks,图片生成从正文占位符改为结构化 intent -> asset -> markdown 回填。 3. 并发与一致性:为 story/chapter intent 增加 claim_token、claimed_at、attempt_count,采用数据库原子 claim 为主、Redis 锁为辅,避免重复生成、锁误删和 processing 卡死。 4. Memoir 读写路径:章节 canonical_markdown 成为正文真源,列表/详情接口补齐 markdown、cover_asset、word_count 等字段,PDF 与 asset 解析链路同步升级。 5. Memory / Retrieval:扩展 transcript ingest、chunking、evidence 检索与 story 聚合基础设施,为后续 story-first RAG 与多 agent 编排提供底座。 6. App 端体验:章节页继续走 MarkdownRenderer 阅读链,同时吸收 fix3-19 的跨平台 UI glitch 修复;更新对话页、首页、文案资源与章节列表映射逻辑。 7. 测试与文档:补充 asset resolver、story image task、章节封面派发、markdown 映射等回归测试,并加入图片占位符退役设计文档。
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text as RNText,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
@@ -350,16 +351,32 @@ function VoiceRecordButton({
|
||||
]}
|
||||
disabled={!enabled}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.voiceRecordLabel,
|
||||
!enabled && styles.voiceRecordLabelDisabled,
|
||||
isRecording && styles.voiceRecordLabelRecording,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{isRecording ? tapToEndLabel : tapToStartLabel}
|
||||
</Text>
|
||||
{isRecording ? (
|
||||
<View style={styles.voiceRecordLabelCenterWrap}>
|
||||
<Text
|
||||
style={[
|
||||
styles.voiceRecordLabel,
|
||||
styles.voiceRecordLabelCenter,
|
||||
styles.voiceRecordLabelRecording,
|
||||
!enabled && styles.voiceRecordLabelDisabled,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tapToEndLabel}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.voiceRecordLabel,
|
||||
!enabled && styles.voiceRecordLabelDisabled,
|
||||
styles.voiceRecordLabelCenter,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tapToStartLabel}
|
||||
</Text>
|
||||
)}
|
||||
{isRecording && (
|
||||
<View style={styles.voiceRecordPill}>
|
||||
<Animated.View
|
||||
@@ -368,11 +385,16 @@ function VoiceRecordButton({
|
||||
{ transform: [{ scale: pulseAnim }] },
|
||||
]}
|
||||
/>
|
||||
{/* TODO: Duration number centering still broken on Android */}
|
||||
<View style={styles.voiceRecordDurationWrap}>
|
||||
<Text style={styles.voiceRecordDuration}>
|
||||
<RNText
|
||||
style={[
|
||||
styles.voiceRecordDuration,
|
||||
Platform.OS === 'android' && styles.voiceRecordDurationAndroid,
|
||||
]}
|
||||
{...(Platform.OS === 'android' && { includeFontPadding: false })}
|
||||
>
|
||||
{formatRecordingDuration(recordingDuration)}
|
||||
</Text>
|
||||
</RNText>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
@@ -505,7 +527,7 @@ function ChatInputBar({
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text
|
||||
style={[styles.sendButtonText, { color: colors.primaryForeground }]}
|
||||
style={[styles.sendButtonText, { color: CHAT_COLORS.onSurface }]}
|
||||
>
|
||||
{sendLabel}
|
||||
</Text>
|
||||
@@ -580,11 +602,21 @@ export default function ConversationScreen() {
|
||||
const [input, setInput] = useState('');
|
||||
const [inputMode, setInputMode] = useState<InputMode>('text');
|
||||
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
const listRef = useRef<FlatList>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onShow = () => setIsKeyboardVisible(true);
|
||||
const onHide = () => setIsKeyboardVisible(false);
|
||||
const onShow = (e: { endCoordinates: { height: number } }) => {
|
||||
setIsKeyboardVisible(true);
|
||||
setKeyboardHeight(e.endCoordinates.height);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
listRef.current?.scrollToEnd({ animated: true });
|
||||
});
|
||||
};
|
||||
const onHide = () => {
|
||||
setIsKeyboardVisible(false);
|
||||
setKeyboardHeight(0);
|
||||
};
|
||||
const subShow = Keyboard.addListener('keyboardDidShow', onShow);
|
||||
const subHide = Keyboard.addListener('keyboardDidHide', onHide);
|
||||
return () => {
|
||||
@@ -621,11 +653,12 @@ export default function ConversationScreen() {
|
||||
|
||||
const keyboardOffset = Platform.OS === 'ios' ? insets.top + 56 : 0;
|
||||
const kavEnabled = inputMode === 'text' && isKeyboardVisible;
|
||||
const kavBehavior = Platform.OS === 'ios' ? 'padding' : 'height';
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={kavEnabled ? 'padding' : undefined}
|
||||
behavior={kavEnabled ? kavBehavior : undefined}
|
||||
keyboardVerticalOffset={keyboardOffset}
|
||||
>
|
||||
<View style={styles.column}>
|
||||
@@ -668,8 +701,14 @@ export default function ConversationScreen() {
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
contentContainerStyle={[
|
||||
styles.listContent,
|
||||
inputMode === 'text' &&
|
||||
isKeyboardVisible && { paddingBottom: 12 + keyboardHeight },
|
||||
]}
|
||||
data={flattenedData}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
keyExtractor={(item) => item.listKey}
|
||||
renderItem={({ item }) => (
|
||||
<MessageBubble
|
||||
@@ -784,6 +823,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 12,
|
||||
@@ -952,6 +992,14 @@ const styles = StyleSheet.create({
|
||||
color: 'rgba(27, 27, 31, 0.72)',
|
||||
flex: 1,
|
||||
},
|
||||
voiceRecordLabelCenter: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
voiceRecordLabelCenterWrap: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
voiceRecordLabelRecording: {
|
||||
color: 'rgba(27, 27, 31, 0.92)',
|
||||
},
|
||||
@@ -980,8 +1028,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
voiceRecordDuration: {
|
||||
fontSize: 12,
|
||||
lineHeight: 12,
|
||||
color: 'rgba(27, 27, 31, 0.86)',
|
||||
textAlign: 'center',
|
||||
},
|
||||
voiceRecordDurationAndroid: {
|
||||
textAlignVertical: 'center',
|
||||
} as const,
|
||||
textInput: {
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
|
||||
Reference in New Issue
Block a user