Skip to content

Commit 0e3bb1d

Browse files
committed
feat: add LLM panel to dataset organizer
1 parent def8302 commit 0e3bb1d

3 files changed

Lines changed: 163 additions & 10 deletions

File tree

backend/src/middleware/auth.middleware.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ const setTokenCookie = (res, user) => {
1212
username: user.username,
1313
};
1414

15+
// Add session start time for new logins
16+
const payload = {
17+
data: safeUser,
18+
};
19+
20+
// If this is a new session, add the session start time
21+
if (isNewSession) {
22+
payload.sessionStart = Math.floor(Date.now() / 1000); // Unix timestamp
23+
} else {
24+
// Preserve the original session start time when refreshing
25+
payload.sessionStart = user.sessionStart;
26+
}
27+
1528
// sign JWT token
1629
const token = jwt.sign({ data: safeUser }, JWT_SECRET, {
1730
expiresIn: parseInt(JWT_EXPIRES_IN),
@@ -54,13 +67,32 @@ const restoreUser = (req, res, next) => {
5467
// extract user id from token payload
5568
const { id } = jwtPayload.data;
5669

70+
// Check maximum session duration (e.g., 24 hours)
71+
const MAX_SESSION_DURATION = parseInt(
72+
process.env.MAX_SESSION_DURATION || "86400"
73+
); // 24 hours default
74+
const currentTime = Math.floor(Date.now() / 1000);
75+
const sessionAge = currentTime - jwtPayload.sessionStart;
76+
77+
if (sessionAge > MAX_SESSION_DURATION) {
78+
// Session has exceeded maximum duration
79+
res.clearCookie("token");
80+
return next();
81+
}
82+
5783
//load user from database
5884
req.user = await User.findByPk(id, {
5985
attributes: {
6086
include: ["id", "username", "email", "created_at", "updated_at"],
6187
exclude: ["hashed_password"], // Never send password
6288
},
6389
});
90+
91+
// refresh token - issue new token with extended expiration
92+
if (req.user) {
93+
req.user.sessionStart = jwtPayload.sessionStart; // Pass along the original session start
94+
setTokenCookie(res, req.user, false);
95+
}
6496
} catch (error) {
6597
res.clearCookie("token");
6698
return next();

src/components/User/Dashboard/DatasetOrganizer/LLMPanel.tsx

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
Alert,
1616
} from "@mui/material";
1717
import { Colors } from "design/theme";
18-
import React, { useState } from "react";
18+
import React, { useState, useEffect } from "react";
1919
import { FileItem } from "redux/projects/types/projects.interface";
2020

2121
interface LLMPanelProps {
@@ -29,47 +29,112 @@ interface LLMProvider {
2929
models: Array<{ id: string; name: string }>;
3030
noApiKey?: boolean;
3131
isAnthropic?: boolean;
32+
customUrl?: boolean;
3233
}
3334

3435
const llmProviders: Record<string, LLMProvider> = {
3536
ollama: {
36-
name: "Ollama (Local)",
37+
name: "Ollama (Local Server)",
3738
baseUrl: "http://localhost:11434/v1/chat/completions",
3839
models: [
40+
{ id: "qwen3-coder:30b", name: "Qwen 3 Coder" },
3941
{ id: "qwen2.5-coder:latest", name: "Qwen 2.5 Coder" },
4042
{ id: "codellama:latest", name: "Code Llama" },
4143
{ id: "llama3.1:latest", name: "Llama 3.1" },
44+
{ id: "mistral:latest", name: "Mistral" },
45+
{ id: "deepseek-coder:latest", name: "DeepSeek Coder" },
4246
],
4347
noApiKey: true,
48+
customUrl: true,
4449
},
4550
groq: {
46-
name: "Groq",
51+
name: "Groq (Free API Key - 14,400 req/day)",
4752
baseUrl: "https://api.groq.com/openai/v1/chat/completions",
4853
models: [
4954
{ id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B" },
5055
{ id: "llama-3.1-8b-instant", name: "Llama 3.1 8B (Fast)" },
56+
{ id: "mixtral-8x7b-32768", name: "Mixtral 8x7B" },
57+
],
58+
},
59+
openrouter: {
60+
name: "OpenRouter (Free models available)",
61+
baseUrl: "https://openrouter.ai/api/v1/chat/completions",
62+
models: [
63+
{
64+
id: "meta-llama/llama-3.1-8b-instruct:free",
65+
name: "Llama 3.1 8B (Free)",
66+
},
67+
{ id: "google/gemma-2-9b-it:free", name: "Gemma 2 9B (Free)" },
68+
{ id: "mistralai/mistral-7b-instruct:free", name: "Mistral 7B (Free)" },
5169
],
5270
},
5371
anthropic: {
54-
name: "Anthropic",
72+
name: "Anthropic Claude (Paid)",
5573
baseUrl: "https://api.anthropic.com/v1/messages",
5674
models: [
5775
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
5876
{ id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku" },
5977
],
6078
isAnthropic: true,
6179
},
80+
openai: {
81+
name: "OpenAI (Paid)",
82+
baseUrl: "https://api.openai.com/v1/chat/completions",
83+
models: [
84+
{ id: "gpt-4o-mini", name: "GPT-4o Mini" },
85+
{ id: "gpt-4o", name: "GPT-4o" },
86+
],
87+
},
6288
};
6389

6490
const LLMPanel: React.FC<LLMPanelProps> = ({ files, onClose }) => {
65-
const [provider, setProvider] = useState<string>("groq");
66-
const [model, setModel] = useState<string>("llama-3.3-70b-versatile");
91+
const [provider, setProvider] = useState<string>("ollama");
92+
const [model, setModel] = useState<string>("qwen3-coder:30b");
93+
const [ollamaUrl, setOllamaUrl] = useState<string>(
94+
"http://huo.neu.edu:11434"
95+
);
6796
const [apiKey, setApiKey] = useState<string>("");
6897
const [generatedScript, setGeneratedScript] = useState<string>("");
6998
const [loading, setLoading] = useState(false);
7099
const [error, setError] = useState<string | null>(null);
71100
const [status, setStatus] = useState<string>("");
72101

102+
const [panelHeight, setPanelHeight] = useState<number>(350);
103+
const [isResizing, setIsResizing] = useState(false);
104+
105+
const handleMouseDown = (e: React.MouseEvent) => {
106+
setIsResizing(true);
107+
e.preventDefault();
108+
};
109+
110+
const handleMouseMove = (e: MouseEvent) => {
111+
if (!isResizing) return;
112+
113+
const newHeight = window.innerHeight - e.clientY;
114+
if (newHeight >= 100 && newHeight <= window.innerHeight - 100) {
115+
setPanelHeight(newHeight);
116+
}
117+
};
118+
119+
const handleMouseUp = () => {
120+
setIsResizing(false);
121+
};
122+
123+
// Add event listeners
124+
useEffect(() => {
125+
if (isResizing) {
126+
document.addEventListener("mousemove", handleMouseMove);
127+
document.addEventListener("mouseup", handleMouseUp);
128+
document.body.style.cursor = "ns-resize";
129+
130+
return () => {
131+
document.removeEventListener("mousemove", handleMouseMove);
132+
document.removeEventListener("mouseup", handleMouseUp);
133+
document.body.style.cursor = "";
134+
};
135+
}
136+
}, [isResizing]);
137+
73138
const currentProvider = llmProviders[provider];
74139

75140
const buildFileSummary = (
@@ -126,7 +191,27 @@ Output ONLY the Python script.`;
126191
try {
127192
let response;
128193

129-
if (currentProvider.isAnthropic) {
194+
if (provider === "ollama") {
195+
const ollamaBaseUrl = ollamaUrl || "http://localhost:11434";
196+
response = await fetch(`${ollamaBaseUrl}/v1/chat/completions`, {
197+
method: "POST",
198+
headers: {
199+
"Content-Type": "application/json",
200+
},
201+
body: JSON.stringify({
202+
model,
203+
messages: [
204+
{
205+
role: "system",
206+
content:
207+
"You are a neuroimaging data expert specializing in BIDS format conversion. Output only Python code.",
208+
},
209+
{ role: "user", content: prompt },
210+
],
211+
stream: false,
212+
}),
213+
});
214+
} else if (currentProvider.isAnthropic) {
130215
response = await fetch(currentProvider.baseUrl, {
131216
method: "POST",
132217
headers: {
@@ -214,14 +299,38 @@ Output ONLY the Python script.`;
214299
bottom: 0,
215300
left: 0,
216301
right: 0,
217-
height: "50vh",
302+
height: `${panelHeight}px`,
218303
zIndex: 1000,
219304
borderTop: 2,
220305
borderColor: Colors.purple,
221306
display: "flex",
222307
flexDirection: "column",
223308
}}
224309
>
310+
{/* Resize Handle */}
311+
<Box
312+
onMouseDown={handleMouseDown}
313+
sx={{
314+
height: 6,
315+
backgroundColor: isResizing ? Colors.lightGray : Colors.lightGray,
316+
cursor: "ns-resize",
317+
display: "flex",
318+
alignItems: "center",
319+
justifyContent: "center",
320+
"&:hover": {
321+
backgroundColor: Colors.lightGray,
322+
},
323+
}}
324+
>
325+
<Box
326+
sx={{
327+
width: 40,
328+
height: 3,
329+
backgroundColor: Colors.secondaryPurple,
330+
// borderRadius: 2,
331+
}}
332+
/>
333+
</Box>
225334
{/* Header */}
226335
<Box
227336
sx={{
@@ -290,6 +399,18 @@ Output ONLY the Python script.`;
290399
</Select>
291400
</FormControl>
292401

402+
{/* ✅ ADD THIS: Ollama Server URL field */}
403+
{provider === "ollama" && (
404+
<TextField
405+
fullWidth
406+
label="Ollama Server URL"
407+
value={ollamaUrl}
408+
onChange={(e) => setOllamaUrl(e.target.value)}
409+
placeholder="http://localhost:11434"
410+
sx={{ mb: 2 }}
411+
/>
412+
)}
413+
293414
{!currentProvider.noApiKey && (
294415
<TextField
295416
fullWidth

src/components/User/Dashboard/DatasetOrganizer/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,9 @@ const DatasetOrganizer: React.FC = () => {
326326
</Box>
327327

328328
{/* LLM Panel */}
329-
{/* {showLLMPanel && (
329+
{showLLMPanel && (
330330
<LLMPanel files={files} onClose={() => setShowLLMPanel(false)} />
331-
)} */}
331+
)}
332332
</Box>
333333
);
334334
};

0 commit comments

Comments
 (0)