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 searchRouter from './search';
import discoverRouter from './discover'; import discoverRouter from './discover';
import uploadsRouter from './uploads'; import uploadsRouter from './uploads';
import translateRouter from './translate';
const router = express.Router(); const router = express.Router();
@ -20,5 +21,6 @@ router.use('/chats', chatsRouter);
router.use('/search', searchRouter); router.use('/search', searchRouter);
router.use('/discover', discoverRouter); router.use('/discover', discoverRouter);
router.use('/uploads', uploadsRouter); router.use('/uploads', uploadsRouter);
router.use('/translate', translateRouter);
export default router; 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, StopCircle,
Layers3, Layers3,
Plus, Plus,
Languages,
Loader2
} from 'lucide-react'; } from 'lucide-react';
import Markdown from 'markdown-to-jsx'; import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy'; import Copy from './MessageActions/Copy';
@ -20,6 +22,7 @@ import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos'; import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech'; import { useSpeech } from 'react-text-to-speech';
import { SearchPDFs } from './SearchPDFs'; import { SearchPDFs } from './SearchPDFs';
import { Select } from './ui/Select';
const MessageBox = ({ const MessageBox = ({
message, message,
@ -42,6 +45,8 @@ const MessageBox = ({
}) => { }) => {
const [parsedMessage, setParsedMessage] = useState(message.content); const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content); const [speechMessage, setSpeechMessage] = useState(message.content);
const [translating, setTranslating] = useState(false);
const [currentLanguage, setCurrentLanguage] = useState('english');
useEffect(() => { useEffect(() => {
const regex = /\[(\d+)\]/g; const regex = /\[(\d+)\]/g;
@ -64,6 +69,32 @@ const MessageBox = ({
setParsedMessage(message.content); setParsedMessage(message.content);
}, [message.content, message.sources, message.role]); }, [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 }); const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
@ -101,6 +132,7 @@ const MessageBox = ({
</div> </div>
)} )}
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
<Disc3 <Disc3
className={cn( className={cn(
@ -113,6 +145,24 @@ const MessageBox = ({
Answer Answer
</h3> </h3>
</div> </div>
<div className="flex items-center space-x-2">
{translating && (
<Loader2 className="animate-spin text-black/70 dark:text-white/70" size={16} />
)}
<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 <Markdown
className={cn( className={cn(
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]', 'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',

View file

@ -10,13 +10,13 @@ export const Select = ({ className, options, ...restProps }: SelectProps) => {
<select <select
{...restProps} {...restProps}
className={cn( 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, className,
)} )}
> >
{options.map(({ label, value, disabled }) => { {options.map(({ label, value, disabled }) => {
return ( return (
<option key={value} value={value} disabled={disabled}> <option key={value} value={value} disabled={disabled} className="py-1">
{label} {label}
</option> </option>
); );