Skip to content
Open
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
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ model User {
subscriptionStart DateTime?
subscriptionEnd DateTime?
apiKey String? @default("null")
apiBaseUrl String? @default("https://api.openai.com/v1")
modelId String? @default("gpt-4o")
Comment on lines 20 to +22
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Prisma schema adds apiBaseUrl and modelId, but there’s no corresponding migration checked in under prisma/migrations. If the project relies on migrations (it looks like it does), this will cause schema drift / deploy failures. Please generate and commit a migration for these new columns.

Copilot uses AI. Check for mistakes.
collections Collection[]
activityDays ActivityDay[]
learnSteps String @default("10m 1d")
Expand Down
24 changes: 16 additions & 8 deletions src/components/app/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import 'highlight.js/styles/atom-one-dark-reasonable.css';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

const ChatWindow = ({
problem,
editorContent,
apiKey,
const ChatWindow = ({
problem,
editorContent,
apiKey,
baseUrl,
modelId,
isTab = false,
externalMessages,
setExternalMessages,
Expand All @@ -19,10 +21,12 @@ const ChatWindow = ({
setExternalIsTyping,
externalShowQuickQuestions,
setExternalShowQuickQuestions
}: {
problem: any,
editorContent: string,
apiKey: any,
}: {
problem: any,
editorContent: string,
apiKey: any,
baseUrl?: any,
modelId?: any,
isTab?: boolean,
externalMessages?: Array<{ text: string, sender: string }>,
setExternalMessages?: React.Dispatch<React.SetStateAction<Array<{ text: string, sender: string }>>>,
Expand Down Expand Up @@ -90,6 +94,8 @@ const ChatWindow = ({
userSolution: editorContent,
userMessage: "analyze", // Special flag to just analyze the code
apiKey: apiKey,
baseUrl: baseUrl,
modelId: modelId,
mode: "analyze" // Tell the API we're just loading context
}),
});
Expand Down Expand Up @@ -156,6 +162,8 @@ const ChatWindow = ({
userSolution: editorContent,
userMessage: initialMessage || input,
apiKey: apiKey,
baseUrl: baseUrl,
modelId: modelId,
mode: "chat" // Specify we're in chat mode now
}),
});
Expand Down
4 changes: 4 additions & 0 deletions src/components/app/Problem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ const Problem = ({ problem, contentActive, setContentActive, editorContent, setE
userSolution: editorContent,
userMessage: "analyze",
apiKey: data?.apiKey,
baseUrl: data?.apiBaseUrl,
modelId: data?.modelId,
mode: "analyze"
}),
});
Expand Down Expand Up @@ -398,6 +400,8 @@ const Problem = ({ problem, contentActive, setContentActive, editorContent, setE
problem={localProblem}
editorContent={editorContent}
apiKey={data?.apiKey}
baseUrl={data?.apiBaseUrl}
modelId={data?.modelId}
isTab={true}
externalMessages={chatMessages}
setExternalMessages={setChatMessages}
Expand Down
2 changes: 2 additions & 0 deletions src/components/app/ProblemModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ const ProblemModal = ({
userSolution: '',
userMessage: `Generate a complete solution for this problem in ${language}. Strictly only provide the code without any explanations, comments, or markdown formatting.`,
apiKey: theUser.apiKey,
baseUrl: theUser.apiBaseUrl,
modelId: theUser.modelId,
mode: 'chat'
}),
});
Expand Down
8 changes: 6 additions & 2 deletions src/components/app/ProblemsQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ const ProblemsQueue = ({ problems, userSettings, refetchProblems }: {problems:an
userSolution: editorContent,
userMessage: "analyze",
apiKey: data?.apiKey,
baseUrl: data?.apiBaseUrl,
modelId: data?.modelId,
mode: "analyze"
}),
});
Expand Down Expand Up @@ -1280,9 +1282,11 @@ const ProblemsQueue = ({ problems, userSettings, refetchProblems }: {problems:an
/>
) : content === 'ai-assistant' ? (
<ChatWindow
problem={dueProblems[0]}
editorContent={editorContent}
problem={dueProblems[0]}
editorContent={editorContent}
apiKey={data?.apiKey}
baseUrl={data?.apiBaseUrl}
modelId={data?.modelId}
isTab={true}
externalMessages={chatMessages}
setExternalMessages={setChatMessages}
Expand Down
64 changes: 61 additions & 3 deletions src/components/app/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const Settings = () => {
const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const [isBaseUrlVisible, setIsBaseUrlVisible] = useState(false);
const [isModelIdVisible, setIsModelIdVisible] = useState(false);

const fetchUserSettings = async () => {
if (!user) throw new Error('No user found');
Expand Down Expand Up @@ -123,6 +125,8 @@ const Settings = () => {
),
// API Settings
apiKey: (document.getElementById('apiKey') as HTMLInputElement)?.value,
apiBaseUrl: (document.getElementById('apiBaseUrl') as HTMLInputElement)?.value,
modelId: (document.getElementById('modelId') as HTMLInputElement)?.value,
};

// Validations (same as before)
Expand Down Expand Up @@ -357,11 +361,11 @@ const Settings = () => {
<KeyIcon className="w-5 h-5 mr-2" style={{ color: '#8b5cf6' }} />
<h2 className="text-lg font-medium text-[#ffffff]">API Settings</h2>
</div>

<div className="space-y-5">
<div className="space-y-2">
<label className="block text-sm font-medium text-[#B0B7C3]">
OpenAI API Key
API Key
</label>
<div className="relative">
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-[rgba(6,182,212,0.1)] to-[rgba(59,130,246,0.1)]" />
Expand All @@ -387,7 +391,61 @@ const Settings = () => {
</div>
</div>
<p className="text-xs text-[#8A94A6]">
Enter your OpenAI API key to enable AI-based features.
Enter your API key to enable AI-based features.
</p>
</div>

<div className="space-y-2">
<label className="block text-sm font-medium text-[#B0B7C3]">
Base URL
</label>
<div className="relative group">
<input
type={isBaseUrlVisible ? "text" : "password"}
id="apiBaseUrl"
defaultValue={data.apiBaseUrl}
placeholder="https://api.openai.com/v1"
className="w-full px-3 py-2 bg-[#2A303C] border border-[#3A4150]/70 rounded-md shadow-sm outline-none focus:outline-none focus:border-[#06b6d4]/70 focus:ring-1 focus:ring-[#3b82f6]/50 transition-all duration-200 text-primary h-11"
style={{ outline: 'none' }}
/>
<button
type="button"
onClick={() => setIsBaseUrlVisible(!isBaseUrlVisible)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#8A94A6] hover:text-[#ffffff] transition-colors"
>
{isBaseUrlVisible ? 'Hide' : 'Show'}
</button>
Comment on lines +404 to +417
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Base URL field is rendered as type="password", but this value isn’t a secret. Using a password input can confuse users and password managers and harms accessibility. Consider using type="text" (and optionally keep a separate “reset to default” affordance) instead of hide/show behavior here.

Suggested change
type={isBaseUrlVisible ? "text" : "password"}
id="apiBaseUrl"
defaultValue={data.apiBaseUrl}
placeholder="https://api.openai.com/v1"
className="w-full px-3 py-2 bg-[#2A303C] border border-[#3A4150]/70 rounded-md shadow-sm outline-none focus:outline-none focus:border-[#06b6d4]/70 focus:ring-1 focus:ring-[#3b82f6]/50 transition-all duration-200 text-primary h-11"
style={{ outline: 'none' }}
/>
<button
type="button"
onClick={() => setIsBaseUrlVisible(!isBaseUrlVisible)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#8A94A6] hover:text-[#ffffff] transition-colors"
>
{isBaseUrlVisible ? 'Hide' : 'Show'}
</button>
type="text"
id="apiBaseUrl"
defaultValue={data.apiBaseUrl}
placeholder="https://api.openai.com/v1"
className="w-full px-3 py-2 bg-[#2A303C] border border-[#3A4150]/70 rounded-md shadow-sm outline-none focus:outline-none focus:border-[#06b6d4]/70 focus:ring-1 focus:ring-[#3b82f6]/50 transition-all duration-200 text-primary h-11"
style={{ outline: 'none' }}
/>

Copilot uses AI. Check for mistakes.
<div className="absolute inset-0 rounded-md border border-[#3b82f6]/0 group-focus-within:border-[#3b82f6]/30 pointer-events-none transition-all duration-300 shadow-[0_0_0_0_rgba(59,130,246,0)] group-focus-within:shadow-[0_0_10px_1px_rgba(59,130,246,0.2)]"></div>
</div>
<p className="text-xs text-[#8A94A6]">
Custom API endpoint for OpenAI-compatible providers (e.g., Ollama, Together AI, Groq). Leave empty for default OpenAI.
</p>
</div>

<div className="space-y-2">
<label className="block text-sm font-medium text-[#B0B7C3]">
Model ID
</label>
<div className="relative group">
<input
type={isModelIdVisible ? "text" : "password"}
id="modelId"
defaultValue={data.modelId}
placeholder="gpt-4o"
className="w-full px-3 py-2 bg-[#2A303C] border border-[#3A4150]/70 rounded-md shadow-sm outline-none focus:outline-none focus:border-[#06b6d4]/70 focus:ring-1 focus:ring-[#3b82f6]/50 transition-all duration-200 text-primary h-11"
style={{ outline: 'none' }}
/>
<button
type="button"
onClick={() => setIsModelIdVisible(!isModelIdVisible)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#8A94A6] hover:text-[#ffffff] transition-colors"
>
{isModelIdVisible ? 'Hide' : 'Show'}
</button>
Comment on lines +431 to +444
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Model ID field is rendered as type="password", but model identifiers aren’t sensitive. This makes it harder to review/edit the value and can interfere with autofill/password managers. Consider switching this input to type="text" and dropping the hide/show toggle.

Suggested change
type={isModelIdVisible ? "text" : "password"}
id="modelId"
defaultValue={data.modelId}
placeholder="gpt-4o"
className="w-full px-3 py-2 bg-[#2A303C] border border-[#3A4150]/70 rounded-md shadow-sm outline-none focus:outline-none focus:border-[#06b6d4]/70 focus:ring-1 focus:ring-[#3b82f6]/50 transition-all duration-200 text-primary h-11"
style={{ outline: 'none' }}
/>
<button
type="button"
onClick={() => setIsModelIdVisible(!isModelIdVisible)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#8A94A6] hover:text-[#ffffff] transition-colors"
>
{isModelIdVisible ? 'Hide' : 'Show'}
</button>
type="text"
id="modelId"
defaultValue={data.modelId}
placeholder="gpt-4o"
className="w-full px-3 py-2 bg-[#2A303C] border border-[#3A4150]/70 rounded-md shadow-sm outline-none focus:outline-none focus:border-[#06b6d4]/70 focus:ring-1 focus:ring-[#3b82f6]/50 transition-all duration-200 text-primary h-11"
style={{ outline: 'none' }}
/>

Copilot uses AI. Check for mistakes.
<div className="absolute inset-0 rounded-md border border-[#3b82f6]/0 group-focus-within:border-[#3b82f6]/30 pointer-events-none transition-all duration-300 shadow-[0_0_0_0_rgba(59,130,246,0)] group-focus-within:shadow-[0_0_10px_1px_rgba(59,130,246,0.2)]"></div>
</div>
<p className="text-xs text-[#8A94A6]">
Model identifier to use (e.g., gpt-4o, llama3.2, mixtral-8x7b). Leave empty for default gpt-4o.
</p>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/pages/api/getUserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
maximumInterval: true,
maximumNewPerDay: true,
apiKey: true,
apiBaseUrl: true,
modelId: true,
contributionHistory: true,
overdueWarningThreshold: true,
currentStreak: true,
Expand Down
7 changes: 4 additions & 3 deletions src/pages/api/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { NextApiRequest, NextApiResponse } from 'next';
import OpenAI from 'openai';

export default async (req: NextApiRequest, res: NextApiResponse) => {
const { question, solution, userSolution, userMessage, apiKey, mode = "chat" } = req.body;
const { question, solution, userSolution, userMessage, apiKey, baseUrl, modelId, mode = "chat" } = req.body;

const openai = new OpenAI({
apiKey: apiKey,
baseURL: baseUrl || "https://api.openai.com/v1",
});
Comment on lines +5 to 10
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseUrl is taken directly from the request body and used as the OpenAI client baseURL. Because this route is unauthenticated, this creates an SSRF / proxy primitive and can also exfiltrate the caller-supplied apiKey to an arbitrary host via the Authorization header. Consider restricting baseUrl to an allowlist (or a server-side config), enforce https, and explicitly block localhost/private/metadata IP ranges; also consider rejecting non-POST requests here to reduce abuse surface.

Copilot uses AI. Check for mistakes.

let messages: any = [];
Expand Down Expand Up @@ -45,9 +46,9 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
}

const completion = await openai.chat.completions.create({
model: "gpt-4o",
model: modelId || "gpt-4o",
messages,
max_tokens: 1500,
max_tokens: 1500,
});

if (completion.choices && completion.choices.length > 0) {
Expand Down
Loading