From 61273953e17518d4e8acaf53c4568071d38c249f Mon Sep 17 00:00:00 2001 From: Khanhlq Date: Mon, 17 Mar 2025 19:41:52 +0700 Subject: [PATCH] feat: add core functionalities for chat, translation, and discovery - Implement chat management with database integration - Add translation API endpoint with language model support - Set up discover route for news aggregation from multiple sources - Configure Docker for development and production environments - Enhance model provider management system - Add suggestion generation functionality --- src/routes/index.ts | 2 + src/routes/translate.ts | 60 ++++++++++++++++++++++++++++++ ui/components/MessageBox.tsx | 72 ++++++++++++++++++++++++++++++------ ui/components/ui/Select.tsx | 4 +- 4 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/routes/translate.ts diff --git a/src/routes/index.ts b/src/routes/index.ts index cb2c915..286569f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,6 +8,7 @@ import chatsRouter from './chats'; import searchRouter from './search'; import discoverRouter from './discover'; import uploadsRouter from './uploads'; +import translateRouter from './translate'; const router = express.Router(); @@ -20,5 +21,6 @@ router.use('/chats', chatsRouter); router.use('/search', searchRouter); router.use('/discover', discoverRouter); router.use('/uploads', uploadsRouter); +router.use('/translate', translateRouter); export default router; diff --git a/src/routes/translate.ts b/src/routes/translate.ts new file mode 100644 index 0000000..f76eb3f --- /dev/null +++ b/src/routes/translate.ts @@ -0,0 +1,60 @@ +import express from 'express'; +import logger from '../utils/logger'; +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { getAvailableChatModelProviders } from '../lib/providers'; +import { ChatOpenAI } from '@langchain/openai'; +import { HumanMessage } from '@langchain/core/messages'; +import { + getCustomOpenaiApiKey, + getCustomOpenaiApiUrl, + getCustomOpenaiModelName, +} from '../config'; + +const router = express.Router(); + +interface TranslateBody { + text: string; + targetLanguage: string; +} + +router.post('/', async (req, res) => { + try { + const body: TranslateBody = req.body; + if (!body.text || !body.targetLanguage) { + return res.status(400).json({ message: 'Missing required fields' }); + } + + const chatModelProviders = await getAvailableChatModelProviders(); + const provider = Object.keys(chatModelProviders)[0]; + const model = Object.keys(chatModelProviders[provider])[0]; + let llm: BaseChatModel | undefined; + + if (provider === 'custom_openai') { + llm = new ChatOpenAI({ + modelName: getCustomOpenaiModelName(), + openAIApiKey: getCustomOpenaiApiKey(), + configuration: { + baseURL: getCustomOpenaiApiUrl(), + }, + }) as unknown as BaseChatModel; + } else { + llm = chatModelProviders[provider][model].model as unknown as BaseChatModel; + } + + if (!llm) { + return res.status(400).json({ message: 'No LLM model available' }); + } + + const prompt = `Translate the following text to ${body.targetLanguage}. Maintain the exact same formatting, including any markdown or special characters: +${body.text}`; + + const response = await llm.invoke([new HumanMessage(prompt)]); + + res.status(200).json({ translatedText: response.content }); + } catch (err: any) { + logger.error(`Error in translation: ${err.message}`); + res.status(500).json({ message: 'An error has occurred.' }); + } +}); + +export default router; \ No newline at end of file diff --git a/ui/components/MessageBox.tsx b/ui/components/MessageBox.tsx index 7adc929..96cfbcc 100644 --- a/ui/components/MessageBox.tsx +++ b/ui/components/MessageBox.tsx @@ -11,6 +11,8 @@ import { StopCircle, Layers3, Plus, + Languages, + Loader2 } from 'lucide-react'; import Markdown from 'markdown-to-jsx'; import Copy from './MessageActions/Copy'; @@ -20,6 +22,7 @@ import SearchImages from './SearchImages'; import SearchVideos from './SearchVideos'; import { useSpeech } from 'react-text-to-speech'; import { SearchPDFs } from './SearchPDFs'; +import { Select } from './ui/Select'; const MessageBox = ({ message, @@ -42,6 +45,8 @@ const MessageBox = ({ }) => { const [parsedMessage, setParsedMessage] = useState(message.content); const [speechMessage, setSpeechMessage] = useState(message.content); + const [translating, setTranslating] = useState(false); + const [currentLanguage, setCurrentLanguage] = useState('english'); useEffect(() => { const regex = /\[(\d+)\]/g; @@ -64,7 +69,33 @@ const MessageBox = ({ setParsedMessage(message.content); }, [message.content, message.sources, message.role]); - + const handleTranslate = async (language: string) => { + if (language === currentLanguage) return; + setTranslating(true); + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/translate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text: message.content, + targetLanguage: language + }), + }); + + if (!response.ok) throw new Error('Translation failed'); + + const data = await response.json(); + setParsedMessage(data.translatedText); + setCurrentLanguage(language); + } catch (error) { + console.error('Translation error:', error); + } finally { + setTranslating(false); + } + }; + const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); return ( @@ -101,17 +132,36 @@ const MessageBox = ({ )}
-
- +
+ +

+ Answer +

+
+
+ {translating && ( + )} - size={20} - /> -

- Answer -

+
+ + {options.map(({ label, value, disabled }) => { return ( - );