fix app-expo code file format

This commit is contained in:
Kevin
2026-05-11 10:25:06 +08:00
parent 219c833157
commit b9425e806b
10 changed files with 374 additions and 375 deletions

View File

@@ -241,7 +241,7 @@ jobs:
echo "$ALIYUN_CR_PASSWORD" | ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ echo "$ALIYUN_CR_PASSWORD" | ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
"docker login $REGISTRY --username=$ALIYUN_CR_USERNAME --password-stdin" "docker login $REGISTRY --username=$ALIYUN_CR_USERNAME --password-stdin"
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=6 -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" " ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "
set -euo pipefail set -euo pipefail
mkdir -p '$COMPOSE_DIR/api' mkdir -p '$COMPOSE_DIR/api'
mkdir -p '$COMPOSE_DIR/api/backups' mkdir -p '$COMPOSE_DIR/api/backups'
@@ -275,7 +275,7 @@ jobs:
scp -P "$SSH_PORT" ./api/docker-compose.yml "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/docker-compose.candidate.yml" scp -P "$SSH_PORT" ./api/docker-compose.yml "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/docker-compose.candidate.yml"
scp -P "$SSH_PORT" "$ENV_SRC" "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/.env.candidate" scp -P "$SSH_PORT" "$ENV_SRC" "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/.env.candidate"
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=6 -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" " ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "
set -euo pipefail set -euo pipefail
cd '$COMPOSE_DIR/api' cd '$COMPOSE_DIR/api'
echo '拉取候选镜像: $IMAGE_TAG' echo '拉取候选镜像: $IMAGE_TAG'
@@ -291,7 +291,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
echo "切换线上版本,容器启动时将自动执行 Alembic..." echo "切换线上版本,容器启动时将自动执行 Alembic..."
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=6 -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" " ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "
set -euo pipefail set -euo pipefail
cd '$COMPOSE_DIR/api' cd '$COMPOSE_DIR/api'
if [ -f '$COMPOSE_FILE' ]; then if [ -f '$COMPOSE_FILE' ]; then
@@ -316,7 +316,9 @@ jobs:
docker compose -f '$COMPOSE_FILE' logs --tail=80 celery-worker || true docker compose -f '$COMPOSE_FILE' logs --tail=80 celery-worker || true
exit 1 exit 1
fi fi
echo '服务启动,输出当前状态...' echo '等待服务启动...'
sleep 20
docker image prune -f || true
docker compose -f '$COMPOSE_FILE' ps docker compose -f '$COMPOSE_FILE' ps
" "

View File

@@ -122,7 +122,10 @@ function UserChatAvatar({
type InputMode = 'text' | 'voice'; type InputMode = 'text' | 'voice';
/** 多段拆条后与后端 `ttsAudioUrls` 下标对齐 */ /** 多段拆条后与后端 `ttsAudioUrls` 下标对齐 */
function assistantBubbleSegmentIndex(item: MessageItem, listKey: string): number { function assistantBubbleSegmentIndex(
item: MessageItem,
listKey: string,
): number {
const part = /_part_(\d+)$/.exec(listKey); const part = /_part_(\d+)$/.exec(listKey);
if (part) return Number(part[1]); if (part) return Number(part[1]);
const seg = /_seg_(\d+)$/.exec(item.id); const seg = /_seg_(\d+)$/.exec(item.id);
@@ -264,7 +267,10 @@ function MessageBubble({
const { t } = useTranslation('conversation'); const { t } = useTranslation('conversation');
const isUser = item.senderType === 'user'; const isUser = item.senderType === 'user';
const isVoice = isVoiceMessage(item); const isVoice = isVoiceMessage(item);
const ttsUrlThisPart = segmentTtsUrlAt(item.ttsAudioUrls, assistantSegmentIndex); const ttsUrlThisPart = segmentTtsUrlAt(
item.ttsAudioUrls,
assistantSegmentIndex,
);
const playbackKind = currentPlaybackItem?.kind; const playbackKind = currentPlaybackItem?.kind;
const playbackRefListKey = currentPlaybackItem?.messageRef?.listKey; const playbackRefListKey = currentPlaybackItem?.messageRef?.listKey;
@@ -276,11 +282,9 @@ function MessageBubble({
playbackMessageRefMatchesMessage(playbackRefListKey, item.id)); playbackMessageRefMatchesMessage(playbackRefListKey, item.id));
const playbackEngaged = playbackIsPlaying || playbackIsPaused; const playbackEngaged = playbackIsPlaying || playbackIsPaused;
const isThisBubbleActiveTts = const isThisBubbleActiveTts = matchesThisMessageForTts && playbackEngaged;
matchesThisMessageForTts && playbackEngaged;
const isThisBubbleTtsPlaying = const isThisBubbleTtsPlaying = isThisBubbleActiveTts && playbackIsPlaying;
isThisBubbleActiveTts && playbackIsPlaying;
const isThisBubbleTtsPaused = isThisBubbleActiveTts && playbackIsPaused; const isThisBubbleTtsPaused = isThisBubbleActiveTts && playbackIsPaused;
const isAssistantTtsHighlight = isThisBubbleActiveTts; const isAssistantTtsHighlight = isThisBubbleActiveTts;
@@ -334,10 +338,7 @@ function MessageBubble({
segmentText: item.content, segmentText: item.content,
}); });
if (!ok) { if (!ok) {
Alert.alert( Alert.alert('', t('readAloudRequestFailed'));
'',
t('readAloudRequestFailed'),
);
} }
} else { } else {
Alert.alert('', t('readAloudNoMessageId')); Alert.alert('', t('readAloudNoMessageId'));
@@ -1088,8 +1089,7 @@ export default function ConversationScreen() {
const { data: profile } = useProfile(); const { data: profile } = useProfile();
const userAvatarUri = useMemo( const userAvatarUri = useMemo(
() => () => resolveApiMediaUrl(user?.avatar_url ?? profile?.avatar_url ?? null),
resolveApiMediaUrl(user?.avatar_url ?? profile?.avatar_url ?? null),
[user?.avatar_url, profile?.avatar_url], [user?.avatar_url, profile?.avatar_url],
); );
const userAvatarLetter = useMemo(() => { const userAvatarLetter = useMemo(() => {
@@ -1287,8 +1287,7 @@ export default function ConversationScreen() {
next[idx] = { next[idx] = {
...target, ...target,
id: nextId, id: nextId,
durableMessageId: durableMessageId: p.assistantMessageId ?? target.durableMessageId,
p.assistantMessageId ?? target.durableMessageId,
ttsAudioUrls: nextUrls, ttsAudioUrls: nextUrls,
}; };
return next; return next;
@@ -1639,7 +1638,9 @@ export default function ConversationScreen() {
<View style={styles.headerTitleBlock}> <View style={styles.headerTitleBlock}>
<RNText <RNText
style={[styles.headerTitle, headerTitleFontStyle]} style={[styles.headerTitle, headerTitleFontStyle]}
{...(Platform.OS === 'android' ? { includeFontPadding: false } : {})} {...(Platform.OS === 'android'
? { includeFontPadding: false }
: {})}
> >
{tApp('name')} {tApp('name')}
</RNText> </RNText>
@@ -1668,7 +1669,9 @@ export default function ConversationScreen() {
backAccessibilityLabel={t('chatTitle')} backAccessibilityLabel={t('chatTitle')}
right={ right={
<View style={styles.headerTtsRow}> <View style={styles.headerTtsRow}>
<RNText style={headerTtsSwitchLabelStyle}>{t('ttsThisTurn')}</RNText> <RNText style={headerTtsSwitchLabelStyle}>
{t('ttsThisTurn')}
</RNText>
<Switch <Switch
accessibilityLabel={t('ttsThisTurnAccessibility')} accessibilityLabel={t('ttsThisTurnAccessibility')}
value={ttsThisTurn} value={ttsThisTurn}
@@ -1715,7 +1718,10 @@ export default function ConversationScreen() {
userAvatarUri={userAvatarUri} userAvatarUri={userAvatarUri}
userAvatarLetter={userAvatarLetter} userAvatarLetter={userAvatarLetter}
conversationId={id ?? ''} conversationId={id ?? ''}
assistantSegmentIndex={assistantBubbleSegmentIndex(item, item.listKey)} assistantSegmentIndex={assistantBubbleSegmentIndex(
item,
item.listKey,
)}
durableAssistantId={durableAssistantIdForBubble(item, id ?? '')} durableAssistantId={durableAssistantIdForBubble(item, id ?? '')}
requestAssistantSegmentTts={requestAssistantSegmentTts} requestAssistantSegmentTts={requestAssistantSegmentTts}
/> />
@@ -1740,8 +1746,7 @@ export default function ConversationScreen() {
agentName={t('agentName')} agentName={t('agentName')}
streamingTtsActive={ streamingTtsActive={
!!streamingMessage && !!streamingMessage &&
(playerStatus === 'playing' || (playerStatus === 'playing' || playerStatus === 'paused') &&
playerStatus === 'paused') &&
currentPlaybackItem?.kind === 'tts_auto' currentPlaybackItem?.kind === 'tts_auto'
} }
onStreamingPress={handleInterruptAssistantTts} onStreamingPress={handleInterruptAssistantTts}

View File

@@ -15,7 +15,10 @@ import {
ScrollView, ScrollView,
View, View,
} from 'react-native'; } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import {
SafeAreaView,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -162,7 +165,10 @@ export default function PersonalInfoScreen() {
? err.message ? err.message
: String(err); : String(err);
if (nicknameCommitted) { if (nicknameCommitted) {
Alert.alert(t('personalInfo.savePartialTitle'), `${t('personalInfo.savePartialBody')}\n\n${msg}`); Alert.alert(
t('personalInfo.savePartialTitle'),
`${t('personalInfo.savePartialBody')}\n\n${msg}`,
);
} else { } else {
Alert.alert(t('personalInfo.saveFailed'), msg); Alert.alert(t('personalInfo.saveFailed'), msg);
} }
@@ -270,7 +276,9 @@ export default function PersonalInfoScreen() {
onPress={() => void handleSave()} onPress={() => void handleSave()}
disabled={saving || profileLoading || !profile} disabled={saving || profileLoading || !profile}
> >
<Text>{saving ? t('personalInfo.saving') : t('personalInfo.save')}</Text> <Text>
{saving ? t('personalInfo.saving') : t('personalInfo.save')}
</Text>
</Button> </Button>
</View> </View>
</ScrollView> </ScrollView>

View File

@@ -511,9 +511,7 @@ export default function ConversationsScreen() {
<Pressable <Pressable
className="items-center gap-6 rounded-2xl bg-muted/30 p-6 active:opacity-90" className="items-center gap-6 rounded-2xl bg-muted/30 p-6 active:opacity-90"
onPress={handleResumeLatestConversation} onPress={handleResumeLatestConversation}
disabled={ disabled={isEnteringChat || createConversation.isPending}
isEnteringChat || createConversation.isPending
}
> >
<Icon <Icon
as={MessageCirclePlus} as={MessageCirclePlus}

View File

@@ -172,10 +172,7 @@ export class WsClient {
return true; return true;
} }
sendText( sendText(text: string, opts?: { ttsThisTurn?: boolean }): boolean {
text: string,
opts?: { ttsThisTurn?: boolean },
): boolean {
return this.send({ return this.send({
type: 'text', type: 'text',
data: { data: {

View File

@@ -67,14 +67,11 @@ export async function buildAvatarUploadFormData(
return form; return form;
} }
form.append( form.append('file', {
'file',
{
uri, uri,
name: filename, name: filename,
type: mime, type: mime,
} as unknown as Blob, } as unknown as Blob);
);
return form; return form;
} }

View File

@@ -188,8 +188,7 @@ export function useUpdateNickname() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (body: UpdateNicknameRequest) => mutationFn: (body: UpdateNicknameRequest) => authApi.updateNickname(body),
authApi.updateNickname(body),
onSuccess: (user) => syncSessionAndProfileQueries(queryClient, user), onSuccess: (user) => syncSessionAndProfileQueries(queryClient, user),
}); });
} }

View File

@@ -143,10 +143,7 @@ export class RealtimeSession {
} }
/** Returns true if the message was sent over the socket. */ /** Returns true if the message was sent over the socket. */
sendText( sendText(text: string, options?: { ttsThisTurn?: boolean }): boolean {
text: string,
options?: { ttsThisTurn?: boolean },
): boolean {
const tts = !!options?.ttsThisTurn; const tts = !!options?.ttsThisTurn;
this.assistantTurnTtsSync = tts; this.assistantTurnTtsSync = tts;
return this.client.sendText(text, { ttsThisTurn: tts }); return this.client.sendText(text, { ttsThisTurn: tts });
@@ -249,7 +246,10 @@ export class RealtimeSession {
}; };
if (this.assistantTurnTtsSync && !payload.manual) { if (this.assistantTurnTtsSync && !payload.manual) {
const idx = event.index ?? 0; const idx = event.index ?? 0;
const key = RealtimeSession.bufferedTtsKey(event.assistantMessageId, idx); const key = RealtimeSession.bufferedTtsKey(
event.assistantMessageId,
idx,
);
this.pendingTtsByKey.set(key, payload); this.pendingTtsByKey.set(key, payload);
} else { } else {
this.onTtsSegment?.(payload); this.onTtsSegment?.(payload);

View File

@@ -1,247 +1,242 @@
// This file is automatically generated by i18next-cli. Do not edit manually. // This file is automatically generated by i18next-cli. Do not edit manually.
interface Resources { interface Resources {
"app": { app: {
"languages": { languages: {
"en": "English", en: 'English';
"system": "System", system: 'System';
"zh": "Chinese" zh: 'Chinese';
}, };
"name": "Life Echo", name: 'Life Echo';
"tabs": { tabs: {
"conversations": "Chats", conversations: 'Chats';
"explore": "Explore", explore: 'Explore';
"home": "Home", home: 'Home';
"memoir": "Memoir", memoir: 'Memoir';
"profile": "Profile" profile: 'Profile';
}, };
"theme": { theme: {
"default": "Default" default: 'Default';
} };
}, };
"auth": { auth: {
"login": { login: {
"codeLabel": "Verification Code", codeLabel: 'Verification Code';
"getCode": "Get Code", getCode: 'Get Code';
"getCodeCountdown": "Retry in {{seconds}}s", getCodeCountdown: 'Retry in {{seconds}}s';
"networkError": "Network error. Please try again later.", networkError: 'Network error. Please try again later.';
"phoneLabel": "Phone Number", phoneLabel: 'Phone Number';
"phonePlaceholder": "Enter your phone number", phonePlaceholder: 'Enter your phone number';
"privacyPolicy": "Privacy Policy", privacyPolicy: 'Privacy Policy';
"submit": "Login", submit: 'Login';
"termsAnd": "and", termsAnd: 'and';
"termsIntro": "I agree to the", termsIntro: 'I agree to the';
"termsRequired": "Please agree to the User Agreement and Privacy Policy first", termsRequired: 'Please agree to the User Agreement and Privacy Policy first';
"termsRequiredConfirm": "OK", termsRequiredConfirm: 'OK';
"termsRequiredTitle": "Agreement Required", termsRequiredTitle: 'Agreement Required';
"userAgreement": "User Agreement", userAgreement: 'User Agreement';
"welcomeSubtitle": "Some lives grow richer the more you savor them.", welcomeSubtitle: 'Some lives grow richer the more you savor them.';
"welcomeTitle": "Welcome back" welcomeTitle: 'Welcome back';
} };
}, };
"common": { common: {
"chapterLabel": "", chapterLabel: '';
"chapterReading": { chapterReading: {
"backgroundColor": "", backgroundColor: '';
"bgPureWhite": "", bgPureWhite: '';
"bgSepia": "", bgSepia: '';
"close": "", close: '';
"fontSize": "", fontSize: '';
"readingSettings": "", readingSettings: '';
"typography": "" typography: '';
}, };
"continueWriting": "", continueWriting: '';
"docs": "Docs", docs: 'Docs';
"emptySubtitle": "", emptySubtitle: '';
"emptyTitle": "", emptyTitle: '';
"readMemory": "", readMemory: '';
"startChapter": "", startChapter: '';
"statusDrafting": "", statusDrafting: '';
"statusLocked": "", statusLocked: '';
"statusPending": "", statusPending: '';
"wordsCount": "" wordsCount: '';
}, };
"conversation": { conversation: {
"addMore": "More", addMore: 'More';
"agentName": "Life Echo", agentName: 'Life Echo';
"assistantReplying": "Replying…", assistantReplying: 'Replying…';
"cancel": "Cancel", cancel: 'Cancel';
"cancelRecording": "Cancel recording", cancelRecording: 'Cancel recording';
"cannotReadAloud": "Read unavailable", cannotReadAloud: 'Read unavailable';
"chatQueueSendTimeout": "Connection timed out. Check your network and try again.", chatQueueSendTimeout: 'Connection timed out. Check your network and try again.';
"chatTitle": "Conversation", chatTitle: 'Conversation';
"chatUnavailableConnecting": "Reconnecting now. You can keep typing and send once the connection is back.", chatUnavailableConnecting: 'Reconnecting now. You can keep typing and send once the connection is back.';
"chatUnavailableDisconnected": "Connection lost. You can keep typing and send after reconnecting.", chatUnavailableDisconnected: 'Connection lost. You can keep typing and send after reconnecting.';
"chatUnavailableTitle": "Chat unavailable", chatUnavailableTitle: 'Chat unavailable';
"confirm": "OK", confirm: 'OK';
"confirmDeleteConversation": "Are you sure you want to delete this conversation? It cannot be recovered.", 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.", createError: 'Unable to create conversation. Please check your network and try again.';
"delete": "Delete", delete: 'Delete';
"deleteConversation": "Delete Conversation", deleteConversation: 'Delete Conversation';
"emptyGreetingSubtitle": "Chat with your companion and record your stories.", emptyGreetingSubtitle: 'Chat with your companion 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';
"readAloudAgain": "Play again", readAloudAgain: 'Play again';
"readAloudPause": "Pause reading", readAloudPause: 'Pause reading';
"readAloudResume": "Resume reading", readAloudResume: 'Resume reading';
"readAloudRequest": "Read aloud", readAloudRequest: 'Read aloud';
"readAloudRequestFailed": "Could not start playback. Check your connection.", readAloudRequestFailed: 'Could not start playback. Check your connection.';
"readAloudNoMessageId": "This message is not ready for on-demand reading yet. Pull to refresh or try again.", readAloudNoMessageId: 'This message is not ready for on-demand reading yet. Pull to refresh or try again.';
"readingAloud": "Reading aloud…", readingAloud: 'Reading aloud…';
"recentChats": "Recent Chats", recentChats: 'Recent Chats';
"recordingPermissionDenied": "Microphone permission is required to record", recordingPermissionDenied: 'Microphone permission is required to record';
"recordingStartFailed": "Unable to start recording. Please try again.", recordingStartFailed: 'Unable to start recording. Please try again.';
"resumeChatSubtitle": "Open your latest conversation to keep talking.", resumeChatSubtitle: 'Open your latest conversation to keep talking.';
"resumeChatTitle": "Continue chatting", resumeChatTitle: 'Continue chatting';
"send": "Send", send: 'Send';
"startNewSubtitle": "Capture a new memory or share your thoughts with your companion.", startNewSubtitle: 'Capture a new memory or share your thoughts with your companion.';
"stopReadingAloud": "Stop reading aloud", stopReadingAloud: 'Stop reading aloud';
"switchToText": "Switch to text input", switchToText: 'Switch to text input';
"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';
"ttsThisTurn": "Speak", ttsThisTurn: 'Speak';
"ttsThisTurnAccessibility": ttsThisTurnAccessibility: 'When on, assistant replies synthesize speech before text appears.';
"When on, assistant replies synthesize speech before text appears.", timeDaysAgo_one: '{{count}} day ago';
"timeDaysAgo_one": "{{count}} day ago", timeDaysAgo_other: '{{count}} days ago';
"timeDaysAgo_other": "{{count}} days ago", timeHoursAgo_one: '{{count}} hour ago';
"timeHoursAgo_one": "{{count}} hour ago", timeHoursAgo_other: '{{count}} hours ago';
"timeHoursAgo_other": "{{count}} hours ago", timeJustNow: 'Just now';
"timeJustNow": "Just now", timeMinutesAgo_one: '{{count}} minute ago';
"timeMinutesAgo_one": "{{count}} minute ago", timeMinutesAgo_other: '{{count}} minutes ago';
"timeMinutesAgo_other": "{{count}} minutes ago", viewAll: 'View All';
"viewAll": "View All", voiceMessagePreview: 'Voice message';
"voiceMessagePreview": "Voice message" };
}, explore: {};
"explore": { home: {};
legal: {
}, titlePrivacy: 'Privacy Policy';
"home": { titleTerms: 'User Agreement';
};
}, memoir: {
"legal": { chapterLabel: 'Chapter {{index}}';
"titlePrivacy": "Privacy Policy", chapterReading: {
"titleTerms": "User Agreement" back: 'Back';
}, backgroundColor: 'Background';
"memoir": { bgPureWhite: 'White';
"chapterLabel": "Chapter {{index}}", bgSepia: 'Sepia';
"chapterReading": { cancel: 'Cancel';
"back": "Back", chapterNotFound: 'Chapter not found';
"backgroundColor": "Background", close: 'Close';
"bgPureWhite": "White", 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.';
"bgSepia": "Sepia", deleteChapter: 'Delete Chapter';
"cancel": "Cancel", deleteChapterAction: 'Delete';
"chapterNotFound": "Chapter not found", fontSans: 'Sans';
"close": "Close", fontSerif: 'Serif';
"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.", fontSize: 'Font Size';
"deleteChapter": "Delete Chapter", fontSizeDefault: 'Medium';
"deleteChapterAction": "Delete", fontSizeLarge: 'Large';
"fontSans": "Sans", fontSizeSmall: 'Small';
"fontSerif": "Serif", readingSettings: 'Reading Settings';
"fontSize": "Font Size", settings: 'Settings';
"fontSizeDefault": "Medium", typography: 'Typography';
"fontSizeLarge": "Large", };
"fontSizeSmall": "Small", continueWriting: 'Continue Writing';
"readingSettings": "Reading Settings", emptySubtitle: 'Chat with your companion to record your stories';
"settings": "Settings", emptyTitle: 'No memoir yet';
"typography": "Typography" frameworkChapters: {
}, chapter1: 'Childhood and upbringing';
"continueWriting": "Continue Writing", chapter2: 'Education and young adulthood';
"emptySubtitle": "Chat with your companion to record your stories", chapter3: 'Early career';
"emptyTitle": "No memoir yet", chapter4: 'Major achievements and peak moments';
"frameworkChapters": { chapter5: 'Setbacks, challenges, and turning points';
"chapter1": "Childhood and upbringing", chapter6: 'Family and relationships';
"chapter2": "Education and young adulthood", chapter7: 'Beliefs and values';
"chapter3": "Early career", chapter8: 'Life summary';
"chapter4": "Major achievements and peak moments", };
"chapter5": "Setbacks, challenges, and turning points", loadErrorMessage: 'Could not load chapters';
"chapter6": "Family and relationships", loadErrorRetry: 'Retry';
"chapter7": "Beliefs and values", pageTitle: 'Memoir';
"chapter8": "Life summary" readMemory: 'Read Memory';
}, startChapter: 'Start Writing';
"loadErrorMessage": "Could not load chapters", statusDrafting: 'Drafting';
"loadErrorRetry": "Retry", statusLocked: 'Locked';
"pageTitle": "Memoir", statusPending: 'Pending';
"readMemory": "Read Memory", wordsCount: '{{count}} words';
"startChapter": "Start Writing", };
"statusDrafting": "Drafting", profile: {
"statusLocked": "Locked", about: {
"statusPending": "Pending", aboutUs: 'About Us';
"wordsCount": "{{count}} words" title: 'About';
}, };
"profile": { appExperience: {
"about": { language: 'Language';
"aboutUs": "About Us", languageDesc: 'Display language';
"title": "About" largeText: 'Large Text';
}, largeTextDesc: 'Make reading easier';
"appExperience": { nightMode: 'Night Mode';
"language": "Language", nightModeDesc: 'Use dark theme';
"languageDesc": "Display language", theme: 'Theme';
"largeText": "Large Text", themeDesc: 'Color theme';
"largeTextDesc": "Make reading easier", title: 'App Experience';
"nightMode": "Night Mode", };
"nightModeDesc": "Use dark theme", dataPrivacy: {
"theme": "Theme", deleteAll: 'Delete All Data';
"themeDesc": "Color theme", deleteUnderDevelopment: 'Delete data feature is under development.';
"title": "App Experience" exportAll: 'Export All Data';
}, exportUnderDevelopment: 'Export feature is under development.';
"dataPrivacy": { purgeDialogCancel: 'Cancel';
"deleteAll": "Delete All Data", purgeDialogConfirm: 'Delete permanently';
"deleteUnderDevelopment": "Delete data feature is under development.", purgeDialogDescription: 'This cannot be undone. Your data will be removed immediately.';
"exportAll": "Export All Data", purgeDialogTitle: 'Final confirmation';
"exportUnderDevelopment": "Export feature is under development.", purgeInputLabel: 'Confirmation phrase';
"purgeDialogCancel": "Cancel", purgeInputPlaceholder: 'Type the phrase shown above';
"purgeDialogConfirm": "Delete permanently", purgeOpenConfirm: 'I understand, continue';
"purgeDialogDescription": "This cannot be undone. Your data will be removed immediately.", purgePhraseHint: 'Type the following Chinese sentence exactly (every character and punctuation). The server only accepts this exact phrase:';
"purgeDialogTitle": "Final confirmation", purgeSubmitting: 'Deleting…';
"purgeInputLabel": "Confirmation phrase", purgeWarningBody: 'This permanently deletes your conversations, memory, stories, chapters, orders, and related files in cloud storage. Profile fields such as birth year, birthplace, where you grew up, and occupation will also be cleared. All devices will be signed out.\nYou can still log in with the same phone number, but your previous content cannot be restored.';
"purgeInputPlaceholder": "Type the phrase shown above", purgeWarningTitle: 'Before you continue';
"purgeOpenConfirm": "I understand, continue", title: 'Data & Privacy';
"purgePhraseHint": "Type the following Chinese sentence exactly (every character and punctuation). The server only accepts this exact phrase:", };
"purgeSubmitting": "Deleting…", editAvatar: 'Edit Profile Picture';
"purgeWarningBody": "This permanently deletes your conversations, memory, stories, chapters, orders, and related files in cloud storage. Profile fields such as birth year, birthplace, where you grew up, and occupation will also be cleared. All devices will be signed out.\nYou can still log in with the same phone number, but your previous content cannot be restored.", helpSupport: {
"purgeWarningTitle": "Before you continue", faq: 'FAQ';
"title": "Data & Privacy" feedback: 'Feedback & Support';
}, feedbackPageTitle: 'Share your thoughts';
"editAvatar": "Edit Profile Picture", title: 'Help & Support';
"helpSupport": { };
"faq": "FAQ", personalInfo: {
"feedback": "Feedback & Support", avatarPresetFailed: 'Could not set preset avatar';
"feedbackPageTitle": "Share your thoughts", avatarUploadFailed: 'Could not upload avatar';
"title": "Help & Support" birthPlacePlaceholder: 'Birthplace';
}, birthYearPlaceholder: 'Birth year';
"personalInfo": { cancel: 'Cancel';
"avatarPresetFailed": "Could not set preset avatar", changeAvatar: 'Change photo';
"avatarUploadFailed": "Could not upload avatar", chooseFromLibrary: 'Choose from library';
"birthPlacePlaceholder": "Birthplace", choosePreset: 'Preset avatars';
"birthYearPlaceholder": "Birth year", grewUpPlaceholder: 'Where you grew up';
"cancel": "Cancel", libraryPermissionDenied: 'Photo library access is required to pick an image';
"changeAvatar": "Change photo", nickname: 'Nickname';
"chooseFromLibrary": "Choose from library", nicknamePlaceholder: 'Enter nickname';
"choosePreset": "Preset avatars", nicknameRequired: 'Please enter a nickname';
"grewUpPlaceholder": "Where you grew up", occupationPlaceholder: 'Occupation';
"libraryPermissionDenied": "Photo library access is required to pick an image", presetPickTitle: 'Choose a preset';
"nickname": "Nickname", save: 'Save';
"nicknamePlaceholder": "Enter nickname", saveFailed: 'Could not save';
"nicknameRequired": "Please enter a nickname", savePartialBody: 'Your nickname was saved, but profile fields below could not be saved. Check your connection and tap Save again.';
"occupationPlaceholder": "Occupation", savePartialTitle: 'Partially saved';
"presetPickTitle": "Choose a preset", saving: 'Saving…';
"save": "Save", title: 'Personal info';
"saveFailed": "Could not save", };
"savePartialBody": "Your nickname was saved, but profile fields below could not be saved. Check your connection and tap Save again.", signOut: 'Sign Out';
"savePartialTitle": "Partially saved", signingOut: 'Signing out...';
"saving": "Saving…", userNamePlaceholder: 'User';
"title": "Personal info" userTier: '{{tier}}';
}, };
"signOut": "Sign Out",
"signingOut": "Signing out...",
"userNamePlaceholder": "User",
"userTier": "{{tier}}"
}
} }
export default Resources; export default Resources;

View File

@@ -98,16 +98,14 @@ describe('conversation entry warmup', () => {
expect(mockLoadMessages).toHaveBeenCalledWith('conv-1'); expect(mockLoadMessages).toHaveBeenCalledWith('conv-1');
expect(mockSessions).toHaveLength(0); expect(mockSessions).toHaveLength(0);
expect(queryClient.getQueryData(conversationKeys.messages('conv-1'))).toEqual( expect(
[existing], queryClient.getQueryData(conversationKeys.messages('conv-1')),
); ).toEqual([existing]);
}); });
test('connects websocket and registers prepared session after opening arrives', async () => { test('connects websocket and registers prepared session after opening arrives', async () => {
const opened = assistantMessage(); const opened = assistantMessage();
mockLoadMessages mockLoadMessages.mockResolvedValueOnce([]).mockResolvedValueOnce([opened]);
.mockResolvedValueOnce([])
.mockResolvedValueOnce([opened]);
mockConnectImpl = ({ conversationId, queryClient }) => { mockConnectImpl = ({ conversationId, queryClient }) => {
queryClient.setQueryData(conversationKeys.messages(conversationId), [ queryClient.setQueryData(conversationKeys.messages(conversationId), [
opened, opened,