feat: 章节软删除、对话左滑删除,移除已读状态
- 章节:详情页增加删除按钮,软删除(is_active=False),AI 不再修改但保留供参考 - 章节:get_chapter 增加 is_active 校验,已删除章节返回 404 - 章节:AI 生成时参考同类别已删除章节摘要 - 对话:左滑显示删除,调用 hard delete API,删除前二次确认 - 对话:根布局包裹 GestureHandlerRootView 以支持 Swipeable - 对话:移除已读/未读状态展示及相关 i18n
This commit is contained in:
@@ -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}
|
||||||
|
|
||||||
## 第一步:提炼核心内容
|
## 第一步:提炼核心内容
|
||||||
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:
|
在改写之前,请先从对话内容中提炼出与人生经历相关的核心信息:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "语音消息"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"chapterLabel": "第 {{index}} 章",
|
"chapterLabel": "第 {{index}} 章",
|
||||||
"chapterReading": {
|
"chapterReading": {
|
||||||
"back": "返回",
|
"back": "返回",
|
||||||
|
"cancel": "取消",
|
||||||
|
"confirmDeleteMessage": "确定要删除本章节吗?删除后您将无法再查看,但内容会保留供后续参考。",
|
||||||
|
"deleteChapter": "删除章节",
|
||||||
|
"deleteChapterAction": "删除",
|
||||||
"backgroundColor": "背景色",
|
"backgroundColor": "背景色",
|
||||||
"bgPureWhite": "白色",
|
"bgPureWhite": "白色",
|
||||||
"bgSepia": "护眼",
|
"bgSepia": "护眼",
|
||||||
|
|||||||
Reference in New Issue
Block a user