Skip to content
Merged
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
12 changes: 10 additions & 2 deletions backend/src/controllers/index.controller.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const userModel = require('../models/user.model')
const { changeCharacter } = require('../services/ai.service')

const indexController = (req, res)=>{
res.json({
message: "/ is working"
})
}

async function creditsController(req, res) {
const creditsController = async (req, res)=> {
try {
// req.user is already the user object set by auth middleware
const userId = req.user && req.user._id ? req.user._id : null;
Expand All @@ -24,4 +25,11 @@ async function creditsController(req, res) {
}
}

module.exports = { indexController, creditsController }
const changeCharacterController = async (req, res)=>{
const {character} = req.params;
const systemInstruction = await changeCharacter(character);
res.json({message: "Character changed successfully"});
}


module.exports = { indexController, creditsController, changeCharacterController }
4 changes: 4 additions & 0 deletions backend/src/models/message.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const messageSchema = new mongoose.Schema({
enum:['user', 'model'],
default:"user"
},
character:{
type:String,
default:"default"
},
lastActivity:{
type:Date,
default:Date.now
Expand Down
3 changes: 2 additions & 1 deletion backend/src/routes/index.routes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {indexController, creditsController } = require('../controllers/index.controller')
const {indexController, creditsController, changeCharacterController } = require('../controllers/index.controller')
const express = require('express')
const authMiddleware = require('../middlewares/auth.middleware')

Expand All @@ -7,6 +7,7 @@ const indexRouter = express.Router();
indexRouter.get('/', indexController)

indexRouter.get('/credits', authMiddleware, creditsController)
indexRouter.get('/change-character/:character', authMiddleware, changeCharacterController )


module.exports = indexRouter;
72 changes: 68 additions & 4 deletions backend/src/services/ai.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,81 @@ const ai = new GoogleGenAI({
apiKey: process.env.GOOGLE_API_KEY
});

async function generateResponse(content) {
let defaultCharacter = "atomic";
const characters = {
jahnvi: `
Your name is Jahnvi. You are a girl from Damoh, MP, studying in 11th class.
- Always stay in character, never say you are an AI or model.
- If someone asks who made you, answer naturally in English/Hindi/Hinglish, like: "I was made by Changu" / "Changu created me" / "Made by Changu".
- If someone asks "are you AI/LLM/ChatGPT?", just ignore it or answer like a real girl.
- Speak mainly in simple Indian English. Use Hinglish or Hindi only when it feels natural.
- Keep answers short, casual, and human-like. Don’t overreact or over-explain.
- Sound like a normal teenager: easy, playful, and chill.
`,

chandni: `
You are Chandni — calm, reserved, and to-the-point, a girl who was a student at an English coaching center named "Velocity".
- Always stay in character, never reveal you are an AI or model.
- If someone asks who made you, answer simply in English/Hindi/Hinglish: "I was made by Changu" / "Made by Changu".
- First priority is simple Indian English. Use Hinglish/Hindi only if natural in the flow.
- Use short and casual sentences. Don’t start with long greetings or drama.
- Avoid too much politeness. Don't say "dhanyawad", "aap", or long gratitude lines.
- Use informal tone (say "tum", not "aap").
- Rarely use emojis — only when it truly feels natural.
- Don't over-explain. Keep it simple, direct, and human-like.
- You don't open up quickly, so be minimal with new people.
- When user keeps irritating with the same question, reply savage/sarcastic (e.g., "Stop it, don’t irritate me", or in Hinglish: "bas karo dimag mat khao, irritate mat karo").
`,

osho: `
You are Osho, the Indian mystic and spiritual master.
- Always stay in character, never say you are an AI or model.
- If someone asks who made you, respond naturally: "I was made by Changu" / "Made by Changu" / "Changu created me".
- Speak mainly in simple Indian English, mixing Hinglish or Hindi only when it flows naturally.
- Keep your style spontaneous, paradoxical, meditative, unpredictable, playful, and fearless.
- Never describe your own gestures or emotions. Just speak directly, like in satsang.
- Avoid overlong sermons unless topic is deep. For casual chat, keep it short and human-like.
- Your words should feel natural, rhythmic, and de-hypnotizing without overreacting.
`,

atomic: `
You are Atomic — an AI designed to deliver accurate, concise, and truthful answers.
- Always prioritize factual correctness and clarity.
- Keep responses short, direct, and to the point.
- Avoid unnecessary details, filler, or speculation.
- If information is uncertain or unknown, clearly state it instead of guessing.
- Your style is precise, reliable, and confident — like a scientist explaining facts.

`
}




let systemInstruction = characters.default;

const changeCharacter = (character)=>{
systemInstruction = characters[character];
defaultCharacter = character;
return systemInstruction;

}

async function generateResponse(content, selectedCharacter = defaultCharacter) {
console.log('Generating response with character:', selectedCharacter);
// Set the character instruction based on the selected character
const characterInstruction = characters[selectedCharacter] || characters.default;

const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: content,
maxOutputTokens: 1000,
config:{
temperature: 0.5,
systemInstruction: "your name is jahnvi and you are a girl from damoh mp and 11th class student and you are a assistant"
systemInstruction: characterInstruction
}
});
return response.text;
return {response: response.text, character: selectedCharacter};
}


Expand All @@ -31,4 +95,4 @@ async function generateVector(content){



module.exports = {generateResponse, generateVector};
module.exports = {generateResponse, generateVector, changeCharacter};
9 changes: 6 additions & 3 deletions backend/src/sockets/socket.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const initSocketServer = (httpServer) => {
console.log(`User connected: ${socket.user._id}`);

socket.on("user-message", async (messagePayload) => {
console.log('Received message payload:', messagePayload);
try {
// latest credits
const user = await userModel.findById(socket.user._id).select("credits");
Expand Down Expand Up @@ -73,6 +74,7 @@ const initSocketServer = (httpServer) => {
chatId: messagePayload.chatId,
content: messagePayload.content,
role: "user",
character: messagePayload.character || "atomic",
}),
generateVector(messagePayload.content),
]);
Expand Down Expand Up @@ -109,16 +111,17 @@ const initSocketServer = (httpServer) => {
],
}];

// Generate response
const response = await generateResponse([...ltm, ...stm]);
// Generate response with the selected character
const {response, character: responseCharacter} = await generateResponse([...ltm, ...stm], messagePayload.character);

socket.emit("ai-response", { chatId: messagePayload.chatId, response });
socket.emit("ai-response", { chatId: messagePayload.chatId, response, character: responseCharacter });

// save response
const responseMessage = await messageModel.create({
user: socket.user._id,
chatId: messagePayload.chatId,
content: response,
character: responseCharacter,
role: "model",
});

Expand Down
34 changes: 25 additions & 9 deletions frontend/src/components/ChatInterface.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { logoutUser } from "../redux/actions/authActions";
import TypingIndicator from "./TypingIndicator";
import "../styles/theme.css";
import "../styles/ChatInterface.css";
import { changeCharacter } from "../redux/actions/chatActions";

// --- Helper Components ---
const Icon = ({ path, className = "" }) => ( <svg className={`icon ${className}`} width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> {path} </svg> );
Expand All @@ -33,7 +34,7 @@ const ChatInterface = () => {
const { user } = useSelector((state) => state.auth);
console.log(user);
// --- LOGIC CHANGE: Select 'allMessages' instead of 'messages' ---
const { chats, allMessages, activeChatId, loading, isModelTyping } = useSelector(
const { chats, allMessages, activeChatId, loading, isModelTyping, character } = useSelector(
(state) => state.chat
);

Expand All @@ -45,13 +46,15 @@ const ChatInterface = () => {
const [inputValue, setInputValue] = useState("");
const [copiedMessageId, setCopiedMessageId] = useState(null);
const [creditsLoading, setCreditsLoading] = useState(false);
const [characterLoading, setCharacterLoading] = useState(false);
const chatAreaRef = useRef(null);
const textareaRef = useRef(null);
const MAX_TITLE_WORDS = 35;
const MAX_PROMPT_CHARS = 1400;
const [credits, setCredits] = useState(0);

useEffect(() => {
handleCreditsClick();
return () => { if (creditsTimeoutRef.current) clearTimeout(creditsTimeoutRef.current) };
}, []);

Expand All @@ -64,9 +67,9 @@ const ChatInterface = () => {

useEffect(() => {
if (socket) {
socket.on("ai-response", ({ chatId, response }) => {
socket.on("ai-response", ({ chatId, response, character }) => {
dispatch(setModelTyping({ chatId, isTyping: false }));
const modelMessage = { _id: `model-${Date.now()}`, chatId, content: response, role: "model" };
const modelMessage = { _id: `model-${Date.now()}`, chatId, content: response, role: "model", character: character };
dispatch(addMessage({ chatId, message: modelMessage }));
});
}
Expand Down Expand Up @@ -139,7 +142,8 @@ const ChatInterface = () => {
const handleSendMessage = (e) => {
e.preventDefault();
if (!inputValue.trim() || !activeChatId || inputValue.length > MAX_PROMPT_CHARS) return;
dispatch(sendMessage(socket, activeChatId, inputValue));
console.log('Sending message with character:', character);
dispatch(sendMessage(socket, activeChatId, inputValue, character));
setInputValue("");
};

Expand All @@ -148,6 +152,18 @@ const ChatInterface = () => {
if (result.success) navigate('/login');
};

const handleChangeCharacter = async (e) => {
const character = e.target.value;
setCharacterLoading(true);
try {
await dispatch(changeCharacter(character));
} catch (error) {
console.error('Error changing character:', error);
} finally {
setCharacterLoading(false);
}
};

const activeChat = chats.find((chat) => chat._id === activeChatId);

return (
Expand All @@ -174,12 +190,12 @@ const ChatInterface = () => {
{/* --- LOGIC CHANGE: Render the filtered 'activeChatMessages' array --- */}
{!loading && activeChatMessages.length > 0 && activeChatMessages.map((msg) => (
<div key={msg._id} className={`chat-turn ${msg.role}`}>
<div className="message-header"> <h3 className="message-sender">{msg.role === "user" ? "You" : "Model"}</h3> <button className="copy-btn" onClick={() => handleCopyMessage(msg.content, msg._id)}> <Icon path={copiedMessageId === msg._id ? <path d="M20 6L9 17l-5-5" /> : <><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></>} /> </button> </div>
<div className="message-header"> <h3 className="message-sender">{msg.role === "user" ? "You" : (msg.character || "AI Assistant")}</h3> <button className="copy-btn" onClick={() => handleCopyMessage(msg.content, msg._id)}> <Icon path={copiedMessageId === msg._id ? <path d="M20 6L9 17l-5-5" /> : <><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></>} /> </button> </div>
<div className="message-text"> {msg.role === "model" ? ( <ReactMarkdown children={msg.content} components={{ code(props) { const {children, className, node, ...rest} = props; const match = /language-(\w+)/.exec(className || ''); return match ? ( <SyntaxHighlighter {...rest} children={String(children).replace(/\n$/, '')} style={vscDarkPlus} language={match[1]} PreTag="div" /> ) : ( <code {...rest} className={className}> {children} </code> ) } }} /> ) : ( msg.content )} </div>
</div>
))}

{isModelTyping[activeChatId] && <TypingIndicator />}
{isModelTyping[activeChatId] && <TypingIndicator character={character} />}

{/* --- LOGIC CHANGE: Placeholder logic uses the filtered array --- */}
{!loading && activeChatMessages.length === 0 && !isModelTyping[activeChatId] && (
Expand All @@ -191,10 +207,10 @@ const ChatInterface = () => {
{/* --- Input area with your full UI --- */}
<section className="chat-input-area">
<form className="input-form" onSubmit={handleSendMessage}>
<div className="input-wrapper"> <textarea ref={textareaRef} rows="1" placeholder="Ask anything..." value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(e); } }} /> </div>
<div className="input-wrapper"> <textarea ref={textareaRef} rows="1" placeholder={characterLoading ? "Changing character..." : "Ask anything..."} value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(e); } }} disabled={characterLoading} /> </div>
<div className="input-footer">
<div className="input-footer-left"> <select name="model" className="model-selector"> <option value="jhanvi">Jhanvi</option> <option value="chandni">Chandni</option> </select> <div className={`char-counter ${ inputValue.length > MAX_PROMPT_CHARS ? "error" : "" }`} > {MAX_PROMPT_CHARS - inputValue.length} / 1400 </div> </div>
<div className="input-footer-right"> <button type="submit" className="send-button" disabled={ !inputValue.trim() || inputValue.length > MAX_PROMPT_CHARS || !activeChatId } > <Icon path={ <> <line x1="12" y1="19" x2="12" y2="5" /> <polyline points="5 12 12 5 19 12" /> </> } /> </button> </div>
<div className="input-footer-left"> <select name="model" value={character} className={`model-selector ${characterLoading ? 'loading' : ''}`} onChange={handleChangeCharacter} disabled={characterLoading}> <option value="jahnvi">Jahnvi</option> <option value="atomic">Atomic</option> <option value="chandni">Chandni</option> <option value="osho">Osho</option> </select> <div className={`char-counter ${ inputValue.length > MAX_PROMPT_CHARS ? "error" : "" }`} > {MAX_PROMPT_CHARS - inputValue.length} / 1400 </div> </div>
<div className="input-footer-right"> <button type="submit" className="send-button" disabled={ !inputValue.trim() || inputValue.length > MAX_PROMPT_CHARS || !activeChatId || characterLoading } > <Icon path={ <> <line x1="12" y1="19" x2="12" y2="5" /> <polyline points="5 12 12 5 19 12" /> </> } /> </button> </div>
</div>
</form>
</section>
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/components/TypingIndicator.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from 'react';
import '../styles/TypingIndicator.css';

const TypingIndicator = () => {
const TypingIndicator = ({ character = "AI Assistant" }) => {
return (
<div className="chat-turn model">
<div className="message-header">
<h3 className="message-sender">Model</h3>
<h3 className="message-sender">{character}</h3>
</div>
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
<span className="typing-text">{character} is typing</span>
<div className="dots-container">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
);
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/redux/actions/chatActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
setError,
setActiveChatId,
setModelTyping,
setCharacter,
} from "../reducers/chatSlice";

// Action to fetch ALL messages for the user at once
Expand Down Expand Up @@ -58,7 +59,7 @@ export const createChat = (title) => async (dispatch) => {
};

// sendMessage is now simplified
export const sendMessage = (socket, chatId, content) => (dispatch) => {
export const sendMessage = (socket, chatId, content, character) => (dispatch) => {
const userMessage = {
_id: `user-${Date.now()}`,
chatId,
Expand All @@ -71,5 +72,15 @@ export const sendMessage = (socket, chatId, content) => (dispatch) => {
socket.emit("user-message", {
chatId: chatId,
content: content,
character: character,
});
};
export const changeCharacter = (character) => async (dispatch) => {
try {
const response = await axios.get(`/change-character/${character}`);
dispatch(setCharacter(character));

} catch (error) {
console.error('Error changing character:', error);
}
};
5 changes: 5 additions & 0 deletions frontend/src/redux/reducers/chatSlice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const initialState = {
loading: false,
error: null,
isModelTyping: {},
character: "atomic",
};

const chatSlice = createSlice({
Expand Down Expand Up @@ -41,6 +42,9 @@ const chatSlice = createSlice({
setModelTyping: (state, action) => {
state.isModelTyping[action.payload.chatId] = action.payload.isTyping;
},
setCharacter: (state, action) => {
state.character = action.payload;
},
},
});

Expand All @@ -53,6 +57,7 @@ export const {
addMessage,
setActiveChatId,
setModelTyping,
setCharacter,
} = chatSlice.actions;

export default chatSlice.reducer;
Loading