feat: 章节软删除、对话左滑删除,移除已读状态

- 章节:详情页增加删除按钮,软删除(is_active=False),AI 不再修改但保留供参考
- 章节:get_chapter 增加 is_active 校验,已删除章节返回 404
- 章节:AI 生成时参考同类别已删除章节摘要
- 对话:左滑显示删除,调用 hard delete API,删除前二次确认
- 对话:根布局包裹 GestureHandlerRootView 以支持 Swipeable
- 对话:移除已读/未读状态展示及相关 i18n
This commit is contained in:
Kevin
2026-03-19 10:44:35 +08:00
parent 1aa3d8593c
commit 9a1d31c71f
12 changed files with 223 additions and 80 deletions

View File

@@ -302,6 +302,7 @@ def get_narrative_prompt(
existing_content: str = "", existing_content: str = "",
user_profile: str = "", user_profile: str = "",
birth_year: Optional[int] = None, birth_year: Optional[int] = None,
archived_summaries: str = "",
) -> str: ) -> str:
"""将新对话改写为叙述(只输出新内容的改写,不重复已有内容)""" """将新对话改写为叙述(只输出新内容的改写,不重复已有内容)"""
context_tail = "" context_tail = ""
@@ -309,6 +310,7 @@ def get_narrative_prompt(
context_tail = existing_content[-300:] if len(existing_content) > 300 else existing_content context_tail = existing_content[-300:] if len(existing_content) > 300 else existing_content
context_section = f"\n\n【衔接上下文(已有内容的末尾,仅供参考衔接,不要重复)】:\n{context_tail}" if context_tail else "" context_section = f"\n\n【衔接上下文(已有内容的末尾,仅供参考衔接,不要重复)】:\n{context_tail}" if context_tail else ""
archived_section = f"\n\n【已删除的该类别历史章节(仅供参考,请勿直接使用或重复)】:\n{archived_summaries}" if archived_summaries else ""
profile_section = f"\n\n用户基本信息:\n{user_profile}" if user_profile else "" profile_section = f"\n\n用户基本信息:\n{user_profile}" if user_profile else ""
age_hint = _build_age_hint(stage, birth_year) age_hint = _build_age_hint(stage, birth_year)
@@ -323,6 +325,7 @@ def get_narrative_prompt(
新的对话内容: 新的对话内容:
{new_content} {new_content}
{context_section} {context_section}
{archived_section}
## 第一步:提炼核心内容 ## 第一步:提炼核心内容
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息: 在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:

View File

@@ -2,7 +2,7 @@
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import Session, joinedload
from app.features.memoir.models import Book, Chapter, ChapterSection, MemoirState from app.features.memoir.models import Book, Chapter, ChapterSection, MemoirState
@@ -56,3 +56,33 @@ async def get_memoir_state(user_id: str, db: AsyncSession) -> MemoirState | None
stmt = select(MemoirState).where(MemoirState.user_id == user_id) stmt = select(MemoirState).where(MemoirState.user_id == user_id)
result = await db.execute(stmt) result = await db.execute(stmt)
return result.scalar_one_or_none() return result.scalar_one_or_none()
def get_archived_chapter_summaries_sync(
session: Session, user_id: str, category: str
) -> list[tuple[str, str]]:
"""获取已删除is_active=False的同类别章节的标题与内容摘要供 AI 参考。"""
stmt = (
select(Chapter)
.where(
Chapter.user_id == user_id,
Chapter.category == category,
Chapter.is_active == False, # noqa: E712
)
.options(joinedload(Chapter.sections))
.order_by(Chapter.updated_at.desc())
)
result = session.execute(stmt)
chapters = list(result.unique().scalars().all())
summaries: list[tuple[str, str]] = []
for ch in chapters:
sections = getattr(ch, "sections", None) or []
parts = [
(s.content or "").strip()
for s in sorted(sections, key=lambda x: getattr(x, "order_index", 0))
]
combined = "".join(parts)
preview = (combined[:200] + "...") if len(combined) > 200 else combined
if preview.strip():
summaries.append((ch.title or "", preview))
return summaries

View File

@@ -170,6 +170,8 @@ class MemoirService:
raise HTTPException(status_code=404, detail="Chapter not found") raise HTTPException(status_code=404, detail="Chapter not found")
if chapter.user_id != user_id: if chapter.user_id != user_id:
raise HTTPException(status_code=403, detail="无权访问此章节") raise HTTPException(status_code=403, detail="无权访问此章节")
if not chapter.is_active:
raise HTTPException(status_code=404, detail="Chapter not found")
await self._cleanup_unavailable_images(chapter) await self._cleanup_unavailable_images(chapter)
return chapter_to_dict(chapter) return chapter_to_dict(chapter)

View File

@@ -23,6 +23,7 @@ from app.features.memoir.models import (
MemoirImage, MemoirImage,
MemoirState, MemoirState,
) )
from app.features.memoir import repo as memoir_repo
from app.features.user.models import User from app.features.user.models import User
from app.core.dependencies import get_llm_provider from app.core.dependencies import get_llm_provider
from app.agents.state_schema import MemoirStateSchema, SlotData, default_state from app.agents.state_schema import MemoirStateSchema, SlotData, default_state
@@ -713,6 +714,12 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
) )
narrative = combined_text narrative = combined_text
# 已删除章节摘要供 AI 参考
archived = memoir_repo.get_archived_chapter_summaries_sync(db, user_id, chapter_category)
archived_summaries = "\n".join(
f"- 《{title_text}》:{preview}" for title_text, preview in archived
) if archived else ""
if llm: if llm:
try: try:
if not chapter: if not chapter:
@@ -733,6 +740,7 @@ def process_memoir_segments(self, user_id: str, segment_ids: List[str]):
existing_content=existing_content, existing_content=existing_content,
user_profile=user_profile, user_profile=user_profile,
birth_year=user_birth_year, birth_year=user_birth_year,
archived_summaries=archived_summaries,
) )
narrative_response = llm.invoke(narrative_prompt) narrative_response = llm.invoke(narrative_prompt)
new_narrative = narrative_response.content.strip() new_narrative = narrative_response.content.strip()
@@ -861,12 +869,18 @@ def generate_chapter_content(self, user_id: str, stage: str, new_content: str):
s.content for s in sorted(chapter.sections, key=lambda x: x.order_index) if (s.content or "").strip() s.content for s in sorted(chapter.sections, key=lambda x: x.order_index) if (s.content or "").strip()
) )
archived = memoir_repo.get_archived_chapter_summaries_sync(db, user_id, stage)
archived_summaries = "\n".join(
f"- 《{title_text}》:{preview}" for title_text, preview in archived
) if archived else ""
if llm: if llm:
prompt = get_narrative_prompt( prompt = get_narrative_prompt(
stage=stage, stage=stage,
slots={}, slots={},
new_content=new_content, new_content=new_content,
existing_content=existing_content, existing_content=existing_content,
archived_summaries=archived_summaries,
) )
response = llm.invoke(prompt) response = llm.invoke(prompt)
new_narrative = response.content.strip() new_narrative = response.content.strip()

View File

@@ -1,10 +1,11 @@
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams } from 'expo-router'; import { router, useLocalSearchParams } from 'expo-router';
import { Settings, X } from 'lucide-react-native'; import { Settings, Trash2, X } from 'lucide-react-native';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Alert,
Modal, Modal,
Platform, Platform,
Pressable, Pressable,
@@ -20,7 +21,7 @@ import { Icon } from '@/components/ui/icon';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { ScreenHeader } from '@/components/screen-header'; import { ScreenHeader } from '@/components/screen-header';
import { ScreenGutter } from '@/constants/layout'; import { ScreenGutter } from '@/constants/layout';
import { useChapterDetail } from '@/features/memoir/hooks'; import { useChapterDetail, useDeleteChapter } from '@/features/memoir/hooks';
// Life-Echo reading colors (from HTML reference) // Life-Echo reading colors (from HTML reference)
const READING_COLORS = { const READING_COLORS = {
@@ -536,6 +537,7 @@ export default function ChapterScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation('memoir'); const { t } = useTranslation('memoir');
const { data: chapter, isLoading } = useChapterDetail(id ?? ''); const { data: chapter, isLoading } = useChapterDetail(id ?? '');
const deleteChapter = useDeleteChapter();
const [settingsVisible, setSettingsVisible] = useState(false); const [settingsVisible, setSettingsVisible] = useState(false);
const [fontSize, setFontSize] = useState<FontSize>('default'); const [fontSize, setFontSize] = useState<FontSize>('default');
@@ -568,6 +570,7 @@ export default function ChapterScreen() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: bgColor, backgroundColor: bgColor,
gap: 16,
}} }}
> >
<Text <Text
@@ -576,6 +579,25 @@ export default function ChapterScreen() {
> >
{t('chapterReading.chapterNotFound')} {t('chapterReading.chapterNotFound')}
</Text> </Text>
<Pressable
onPress={() => router.back()}
style={({ pressed }) => ({
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 12,
backgroundColor: READING_COLORS.primary,
opacity: pressed ? 0.8 : 1,
})}
accessibilityRole="button"
accessibilityLabel={t('chapterReading.back')}
>
<Text
style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}
selectable={false}
>
{t('chapterReading.back')}
</Text>
</Pressable>
</View> </View>
); );
} }
@@ -583,6 +605,25 @@ export default function ChapterScreen() {
const sections = chapter.sections ?? []; const sections = chapter.sections ?? [];
const coverImageUrl = chapter.cover_image?.url ?? null; const coverImageUrl = chapter.cover_image?.url ?? null;
const handleDeletePress = () => {
Alert.alert(
t('chapterReading.deleteChapter'),
t('chapterReading.confirmDeleteMessage'),
[
{ text: t('chapterReading.cancel'), style: 'cancel' },
{
text: t('chapterReading.deleteChapterAction'),
style: 'destructive',
onPress: () => {
deleteChapter.mutate(chapter.id, {
onSuccess: () => router.back(),
});
},
},
],
);
};
return ( return (
<View style={{ flex: 1, backgroundColor: bgColor }}> <View style={{ flex: 1, backgroundColor: bgColor }}>
<ScreenHeader <ScreenHeader
@@ -592,19 +633,38 @@ export default function ChapterScreen() {
title={chapter.title} title={chapter.title}
backAccessibilityLabel={t('chapterReading.back')} backAccessibilityLabel={t('chapterReading.back')}
right={ right={
<Pressable <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
onPress={() => setSettingsVisible(true)} <Pressable
style={({ pressed }) => ({ onPress={handleDeletePress}
padding: 8, disabled={deleteChapter.isPending}
marginRight: -8, style={({ pressed }) => ({
borderRadius: 9999, padding: 8,
opacity: pressed ? 0.7 : 1, borderRadius: 9999,
})} opacity: pressed ? 0.7 : 1,
accessibilityLabel={t('chapterReading.settings')} })}
accessibilityRole="button" accessibilityLabel={t('chapterReading.deleteChapter')}
> accessibilityRole="button"
<Icon as={Settings} size={24} color={READING_COLORS.primary} /> >
</Pressable> <Icon
as={Trash2}
size={22}
color={READING_COLORS.onSurfaceVariant}
/>
</Pressable>
<Pressable
onPress={() => setSettingsVisible(true)}
style={({ pressed }) => ({
padding: 8,
marginRight: -8,
borderRadius: 9999,
opacity: pressed ? 0.7 : 1,
})}
accessibilityLabel={t('chapterReading.settings')}
accessibilityRole="button"
>
<Icon as={Settings} size={24} color={READING_COLORS.primary} />
</Pressable>
</View>
} }
/> />

View File

@@ -3,7 +3,8 @@ import React from 'react';
import { Alert, Pressable, ScrollView, View } from 'react-native'; import { Alert, Pressable, ScrollView, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CheckCheck, MessageCirclePlus, User } from 'lucide-react-native'; import { Swipeable } from 'react-native-gesture-handler';
import { MessageCirclePlus, Trash2, User } from 'lucide-react-native';
import { Icon } from '@/components/ui/icon'; import { Icon } from '@/components/ui/icon';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
@@ -12,6 +13,7 @@ import { NetworkError } from '@/core/api/types';
import { import {
useConversations, useConversations,
useCreateConversation, useCreateConversation,
useDeleteConversation,
} from '@/features/conversation/hooks'; } from '@/features/conversation/hooks';
import type { ConversationListItem } from '@/features/conversation/types'; import type { ConversationListItem } from '@/features/conversation/types';
@@ -65,43 +67,13 @@ function GreetingCardSkeleton() {
); );
} }
function StatusBadge({
status,
label,
}: {
status: 'read' | 'unread';
label: string;
}) {
const isPrimary = status === 'unread';
return (
<View className="flex-row items-center gap-1.5">
{status === 'read' && (
<Icon as={CheckCheck} className="text-primary" size={14} />
)}
{status === 'unread' && (
<View className="h-2 w-2 rounded-full bg-primary" />
)}
<Text
className={`text-xs font-semibold uppercase tracking-wider ${
isPrimary ? 'text-primary' : 'text-muted-foreground'
}`}
>
{label}
</Text>
</View>
);
}
function ConversationCard({ function ConversationCard({
item, item,
statusLabel,
onPress, onPress,
}: { }: {
item: ConversationListItem; item: ConversationListItem;
statusLabel: string;
onPress: () => void; onPress: () => void;
}) { }) {
const status = item.unreadCount > 0 ? ('unread' as const) : ('read' as const);
const avatarBg = item.isDefaultAssistant ? 'bg-primary' : 'bg-secondary'; const avatarBg = item.isDefaultAssistant ? 'bg-primary' : 'bg-secondary';
const avatarIconClass = item.isDefaultAssistant const avatarIconClass = item.isDefaultAssistant
? 'text-primary-foreground' ? 'text-primary-foreground'
@@ -136,14 +108,56 @@ function ConversationCard({
> >
{item.latestMessagePreview || ''} {item.latestMessagePreview || ''}
</Text> </Text>
<View className="mt-2">
<StatusBadge status={status} label={statusLabel} />
</View>
</View> </View>
</Pressable> </Pressable>
); );
} }
function SwipeableConversationCard({
item,
onPress,
}: {
item: ConversationListItem;
onPress: () => void;
}) {
const { t } = useTranslation('conversation');
const deleteConversation = useDeleteConversation();
const handleDelete = () => {
Alert.alert(t('deleteConversation'), t('confirmDeleteConversation'), [
{ text: t('cancel'), style: 'cancel' },
{
text: t('delete'),
style: 'destructive',
onPress: () => deleteConversation.mutate(item.id),
},
]);
};
const renderRightActions = () => (
<Pressable
onPress={handleDelete}
disabled={deleteConversation.isPending}
className="w-20 items-center justify-center rounded-xl bg-destructive active:opacity-80"
style={{ marginLeft: 4 }}
>
<Icon as={Trash2} className="text-destructive-foreground" size={24} />
<Text
className="mt-1 text-xs font-semibold text-destructive-foreground"
selectable={false}
>
{t('delete')}
</Text>
</Pressable>
);
return (
<Swipeable renderRightActions={renderRightActions} friction={2}>
<ConversationCard item={item} onPress={onPress} />
</Swipeable>
);
}
const SKELETON_COUNT = 3; const SKELETON_COUNT = 3;
export default function ConversationsScreen() { export default function ConversationsScreen() {
@@ -277,12 +291,9 @@ export default function ConversationsScreen() {
</View> </View>
<View className="gap-4"> <View className="gap-4">
{conversations.map((item) => ( {conversations.map((item) => (
<ConversationCard <SwipeableConversationCard
key={item.id} key={item.id}
item={item} item={item}
statusLabel={
item.unreadCount > 0 ? t('unread') : t('read')
}
onPress={() => handleConversationPress(item.id)} onPress={() => handleConversationPress(item.id)}
/> />
))} ))}

View File

@@ -2,6 +2,7 @@ import { PortalHost } from '@rn-primitives/portal';
import { Stack } from 'expo-router'; import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { import {
SafeAreaProvider, SafeAreaProvider,
initialWindowMetrics, initialWindowMetrics,
@@ -43,23 +44,25 @@ export default function RootLayout() {
}, [setColorScheme]); }, [setColorScheme]);
return ( return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}> <GestureHandlerRootView style={{ flex: 1 }}>
<AppProviders> <SafeAreaProvider initialMetrics={initialWindowMetrics}>
<TypographyProvider> <AppProviders>
<ThemeVariablesProvider> <TypographyProvider>
<NavigationThemeProvider> <ThemeVariablesProvider>
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} /> <NavigationThemeProvider>
<AnimatedSplashOverlay /> <StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
<Stack screenOptions={{ headerShown: false }}> <AnimatedSplashOverlay />
<Stack.Screen name="(tabs)" /> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" /> <Stack.Screen name="(tabs)" />
<Stack.Screen name="(main)" /> <Stack.Screen name="(auth)" />
</Stack> <Stack.Screen name="(main)" />
<PortalHost /> </Stack>
</NavigationThemeProvider> <PortalHost />
</ThemeVariablesProvider> </NavigationThemeProvider>
</TypographyProvider> </ThemeVariablesProvider>
</AppProviders> </TypographyProvider>
</SafeAreaProvider> </AppProviders>
</SafeAreaProvider>
</GestureHandlerRootView>
); );
} }

View File

@@ -15,7 +15,6 @@ interface Resources {
profile: 'Profile'; profile: 'Profile';
}; };
theme: { theme: {
brand: 'Brand';
default: 'Default'; default: 'Default';
}; };
}; };
@@ -64,17 +63,22 @@ interface Resources {
conversation: { conversation: {
addMore: 'More'; addMore: 'More';
agentName: 'Life Echo'; agentName: 'Life Echo';
cancel: 'Cancel';
cancelRecording: 'Cancel recording'; cancelRecording: 'Cancel recording';
chatTitle: 'Conversation'; chatTitle: 'Conversation';
confirm: 'OK';
confirmDeleteConversation: 'Are you sure you want to delete this conversation? It cannot be recovered.';
connectionConnected: 'Connected'; connectionConnected: 'Connected';
connectionConnecting: 'Connecting...'; connectionConnecting: 'Connecting...';
connectionDisconnected: 'Disconnected'; connectionDisconnected: 'Disconnected';
createError: 'Unable to create conversation. Please check your network and try again.';
delete: 'Delete';
deleteConversation: 'Delete Conversation';
emptyGreetingSubtitle: 'Chat with Echo and record your stories.'; emptyGreetingSubtitle: 'Chat with Echo and record your stories.';
greetingTitle: 'Say Hello'; greetingTitle: 'Say Hello';
inputPlaceholder: 'Type a message...'; inputPlaceholder: 'Type a message...';
inputPlaceholderVoice: 'Type here or hold the mic to speak...'; inputPlaceholderVoice: 'Type here or hold the mic to speak...';
me: 'Me'; me: 'Me';
read: 'Read';
recentChats: 'Recent Chats'; recentChats: 'Recent Chats';
recordingPermissionDenied: 'Microphone permission is required to record'; recordingPermissionDenied: 'Microphone permission is required to record';
send: 'Send'; send: 'Send';
@@ -83,8 +87,8 @@ interface Resources {
switchToVoice: 'Switch to voice input'; switchToVoice: 'Switch to voice input';
tapToEndRecording: 'Tap to end'; tapToEndRecording: 'Tap to end';
tapToStartRecording: 'Tap to start recording'; tapToStartRecording: 'Tap to start recording';
unread: 'Unread';
viewAll: 'View All'; viewAll: 'View All';
voiceMessagePreview: 'Voice message';
}; };
explore: {}; explore: {};
home: {}; home: {};
@@ -95,8 +99,12 @@ interface Resources {
backgroundColor: 'Background'; backgroundColor: 'Background';
bgPureWhite: 'White'; bgPureWhite: 'White';
bgSepia: 'Sepia'; bgSepia: 'Sepia';
cancel: 'Cancel';
chapterNotFound: 'Chapter not found'; chapterNotFound: 'Chapter not found';
close: 'Close'; close: 'Close';
confirmDeleteMessage: 'Are you sure you want to delete this chapter? You will no longer be able to view it, but the content will be kept for future reference.';
deleteChapter: 'Delete Chapter';
deleteChapterAction: 'Delete';
fontSans: 'Sans'; fontSans: 'Sans';
fontSerif: 'Serif'; fontSerif: 'Serif';
fontSize: 'Font Size'; fontSize: 'Font Size';

View File

@@ -1,6 +1,10 @@
{ {
"confirmDeleteConversation": "Are you sure you want to delete this conversation? It cannot be recovered.",
"createError": "Unable to create conversation. Please check your network and try again.", "createError": "Unable to create conversation. Please check your network and try again.",
"confirm": "OK", "confirm": "OK",
"cancel": "Cancel",
"delete": "Delete",
"deleteConversation": "Delete Conversation",
"addMore": "More", "addMore": "More",
"agentName": "Life Echo", "agentName": "Life Echo",
"cancelRecording": "Cancel recording", "cancelRecording": "Cancel recording",
@@ -13,7 +17,6 @@
"inputPlaceholder": "Type a message...", "inputPlaceholder": "Type a message...",
"inputPlaceholderVoice": "Type here or hold the mic to speak...", "inputPlaceholderVoice": "Type here or hold the mic to speak...",
"me": "Me", "me": "Me",
"read": "Read",
"recentChats": "Recent Chats", "recentChats": "Recent Chats",
"recordingPermissionDenied": "Microphone permission is required to record", "recordingPermissionDenied": "Microphone permission is required to record",
"send": "Send", "send": "Send",
@@ -22,7 +25,6 @@
"switchToVoice": "Switch to voice input", "switchToVoice": "Switch to voice input",
"tapToEndRecording": "Tap to end", "tapToEndRecording": "Tap to end",
"tapToStartRecording": "Tap to start recording", "tapToStartRecording": "Tap to start recording",
"unread": "Unread",
"viewAll": "View All", "viewAll": "View All",
"voiceMessagePreview": "Voice message" "voiceMessagePreview": "Voice message"
} }

View File

@@ -2,6 +2,10 @@
"chapterLabel": "Chapter {{index}}", "chapterLabel": "Chapter {{index}}",
"chapterReading": { "chapterReading": {
"back": "Back", "back": "Back",
"cancel": "Cancel",
"confirmDeleteMessage": "Are you sure you want to delete this chapter? You will no longer be able to view it, but the content will be kept for future reference.",
"deleteChapter": "Delete Chapter",
"deleteChapterAction": "Delete",
"backgroundColor": "Background", "backgroundColor": "Background",
"bgPureWhite": "White", "bgPureWhite": "White",
"bgSepia": "Sepia", "bgSepia": "Sepia",

View File

@@ -1,6 +1,10 @@
{ {
"confirmDeleteConversation": "确定要删除此对话吗?删除后无法恢复。",
"createError": "无法创建对话,请检查网络连接或稍后重试", "createError": "无法创建对话,请检查网络连接或稍后重试",
"confirm": "知道了", "confirm": "知道了",
"cancel": "取消",
"delete": "删除",
"deleteConversation": "删除对话",
"addMore": "更多功能", "addMore": "更多功能",
"agentName": "岁月知己", "agentName": "岁月知己",
"cancelRecording": "取消录音发送", "cancelRecording": "取消录音发送",
@@ -13,7 +17,6 @@
"inputPlaceholder": "输入消息...", "inputPlaceholder": "输入消息...",
"inputPlaceholderVoice": "点击这里输入,或者按住左边说话...", "inputPlaceholderVoice": "点击这里输入,或者按住左边说话...",
"me": "我", "me": "我",
"read": "已读",
"recentChats": "最近对话", "recentChats": "最近对话",
"recordingPermissionDenied": "需要麦克风权限才能录音", "recordingPermissionDenied": "需要麦克风权限才能录音",
"send": "发送", "send": "发送",
@@ -22,7 +25,6 @@
"switchToVoice": "切换到语音输入", "switchToVoice": "切换到语音输入",
"tapToEndRecording": "点击结束", "tapToEndRecording": "点击结束",
"tapToStartRecording": "点击开始录音", "tapToStartRecording": "点击开始录音",
"unread": "未读",
"viewAll": "查看全部", "viewAll": "查看全部",
"voiceMessagePreview": "语音消息" "voiceMessagePreview": "语音消息"
} }

View File

@@ -2,6 +2,10 @@
"chapterLabel": "第 {{index}} 章", "chapterLabel": "第 {{index}} 章",
"chapterReading": { "chapterReading": {
"back": "返回", "back": "返回",
"cancel": "取消",
"confirmDeleteMessage": "确定要删除本章节吗?删除后您将无法再查看,但内容会保留供后续参考。",
"deleteChapter": "删除章节",
"deleteChapterAction": "删除",
"backgroundColor": "背景色", "backgroundColor": "背景色",
"bgPureWhite": "白色", "bgPureWhite": "白色",
"bgSepia": "护眼", "bgSepia": "护眼",