Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const leftActions: ActionKeys[] = [
'model',
'search',
'typo',
'inputTranslate',
'fileUpload',
'knowledgeBase',
'tools',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const leftActions: ActionKeys[] = [
'model',
'search',
'typo',
'inputTranslate',
'fileUpload',
'knowledgeBase',
'tools',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useSend } from '../useSend';
const leftActions: ActionKeys[] = [
'model',
'search',
'inputTranslate',
'fileUpload',
'knowledgeBase',
'tools',
Expand Down
46 changes: 46 additions & 0 deletions src/features/ChatInput/ActionBar/InputTranslate/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { LanguagesIcon } from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { useChatInputStore } from '../../store';
import Action from '../components/Action';
import { useInputTranslate } from './useInputTranslate';

const InputTranslate = memo(() => {
const { t } = useTranslation('chat');
const [isTranslating, setIsTranslating] = useState(false);
const editor = useChatInputStore((s) => s.editor);
const { translateToEnglish } = useInputTranslate();

const handleTranslate = async () => {
if (!editor || isTranslating) return;

const content = editor.getDocument('markdown') as string;
if (!content?.trim()) return;

setIsTranslating(true);
try {
const translatedContent = await translateToEnglish(content);
if (translatedContent && translatedContent !== content) {
editor.setDocument('markdown', translatedContent);
}
Comment on lines +21 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Check for translatedContent equality may not handle whitespace or formatting changes.

Normalize both strings before comparison to avoid false negatives due to formatting or whitespace differences.

Suggested change
setIsTranslating(true);
try {
const translatedContent = await translateToEnglish(content);
if (translatedContent && translatedContent !== content) {
editor.setDocument('markdown', translatedContent);
}
// Helper to normalize whitespace and formatting
const normalize = (str: string) =>
str.replace(/\s+/g, ' ').trim();
setIsTranslating(true);
try {
const translatedContent = await translateToEnglish(content);
if (
translatedContent &&
normalize(translatedContent) !== normalize(content)
) {
editor.setDocument('markdown', translatedContent);
}

} catch (error) {
console.error('Translation failed:', error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Logging errors to console may not be sufficient for user feedback.

Please add a user-facing notification or UI message when translation fails to ensure users are aware of the error.

Suggested implementation:

    setErrorMessage('');
    setIsTranslating(true);
    try {
      const translatedContent = await translateToEnglish(content);
      if (translatedContent && translatedContent !== content) {
        editor.setDocument('markdown', translatedContent);
      }
    } catch (error) {
      console.error('Translation failed:', error);
      setErrorMessage('Translation failed. Please try again.');
    } finally {
      setIsTranslating(false);
    }
  };
  return (
    <>
      <Action
        icon={LanguagesIcon}
        loading={isTranslating}
        onClick={handleTranslate}
        title={t('input.translateToEnglish')}
      />
      {errorMessage && (
        <div style={{ color: 'red', marginTop: 8 }}>
          {errorMessage}
        </div>
      )}
    </>
    const [errorMessage, setErrorMessage] = React.useState('');
    const content = editor.getDocument('markdown') as string;
    if (!content?.trim()) return;

} finally {
setIsTranslating(false);
}
};

return (
<Action
icon={LanguagesIcon}
loading={isTranslating}
onClick={handleTranslate}
title={t('input.translateToEnglish')}
/>
);
});

InputTranslate.displayName = 'InputTranslate';

export default InputTranslate;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { chainTranslate } from '@lobechat/prompts';
import { TraceNameMap } from '@lobechat/types';
import { useCallback } from 'react';

import { chatService } from '@/services/chat';
import { useChatStore } from '@/store/chat';
import { useUserStore } from '@/store/user';
import { systemAgentSelectors } from '@/store/user/selectors';
import { merge } from '@/utils/merge';

export const useInputTranslate = () => {
const getCurrentTracePayload = useChatStore((s) => s.getCurrentTracePayload);

const translateToEnglish = useCallback(
async (content: string): Promise<string> => {
return new Promise((resolve, reject) => {
// Get current translation settings
const translationSetting = systemAgentSelectors.translation(useUserStore.getState());

let translatedContent = '';

chatService
.fetchPresetTaskResult({
onFinish: (result) => {
if (result && typeof result === 'string') {
resolve(result);
} else {
resolve(translatedContent);
}
},
onMessageHandle: (chunk) => {
if (chunk.type === 'text') {
translatedContent += chunk.text;
}
},
params: merge(translationSetting, chainTranslate(content, 'en-US')),
trace: getCurrentTracePayload({ traceName: TraceNameMap.Translator }),
})
.catch((error) => {
reject(error);
Comment on lines +22 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject translation promise on failure

The hook wraps chatService.fetchPresetTaskResult in a new promise that only resolves inside onFinish and rejects via .catch. However fetchPresetTaskResult swallows errors and invokes onError without rejecting, so when the translation call fails (e.g. network/provider error) onFinish is never triggered and the wrapper promise never settles. Because InputTranslate awaits this promise to toggle isTranslating, a failed request leaves the action permanently stuck in the loading state. Provide an onError handler that calls reject (or resolve to a fallback) so the UI can recover after errors.

Useful? React with 👍 / 👎.

});
});
},
[getCurrentTracePayload],
);

return {
translateToEnglish,
};
};
2 changes: 2 additions & 0 deletions src/features/ChatInput/ActionBar/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Clear from './Clear';
import History from './History';
import InputTranslate from './InputTranslate';
import Knowledge from './Knowledge';
import Model from './Model';
import Params from './Params';
Expand All @@ -15,6 +16,7 @@ export const actionMap = {
clear: Clear,
fileUpload: Upload,
history: History,
inputTranslate: InputTranslate,
knowledgeBase: Knowledge,
mainToken: MainToken,
model: Model,
Expand Down
1 change: 1 addition & 0 deletions src/locales/default/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default {
sendWithCmdEnter: '按 <key/> 键发送',
sendWithEnter: '按 <key/> 键发送',
stop: '停止',
translateToEnglish: '翻译成英文',
warp: '换行',
},
intentUnderstanding: {
Expand Down
Loading