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
This commit is contained in:
Khanhlq 2025-03-17 19:41:52 +07:00
parent 4d438f06cd
commit 61273953e1
4 changed files with 125 additions and 13 deletions

View file

@ -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;

60
src/routes/translate.ts Normal file
View file

@ -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;

View file

@ -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 = ({
</div>
)}
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
'text-black dark:text-white',
isLast && loading ? 'animate-spin' : 'animate-none',
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
'text-black dark:text-white',
isLast && loading ? 'animate-spin' : 'animate-none',
)}
size={20}
/>
<h3 className="text-black dark:text-white font-medium text-xl">
Answer
</h3>
</div>
<div className="flex items-center space-x-2">
{translating && (
<Loader2 className="animate-spin text-black/70 dark:text-white/70" size={16} />
)}
size={20}
/>
<h3 className="text-black dark:text-white font-medium text-xl">
Answer
</h3>
<div className="flex items-center space-x-2">
<Languages size={16} className="text-black/70 dark:text-white/70" />
<Select
value={currentLanguage}
onChange={(e) => handleTranslate(e.target.value)}
className="h-auto text-sm py-1 min-w-[120px]" // Adjusted height and padding
options={[
{ value: 'english', label: 'English' },
{ value: 'russian', label: 'Russian' }
]}
/>
</div>
</div>
</div>
<Markdown
className={cn(

View file

@ -10,13 +10,13 @@ export const Select = ({ className, options, ...restProps }: SelectProps) => {
<select
{...restProps}
className={cn(
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
'bg-light-secondary dark:bg-dark-secondary px-3 py-1 flex items-center overflow-visible border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm min-w-[120px] leading-normal',
className,
)}
>
{options.map(({ label, value, disabled }) => {
return (
<option key={value} value={value} disabled={disabled}>
<option key={value} value={value} disabled={disabled} className="py-1">
{label}
</option>
);