feat: implement QuestionBank CRUD with pagination and template query

- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
import React, { useState } from 'react';
import { BookOpen } from 'lucide-react';
import { motion } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { cn } from '../../utils/cn';
export default function Login() {
const { login, user } = useAuth();
const navigate = useNavigate();
const [apiKeyInput, setApiKeyInput] = useState('');
const [loginUsername, setLoginUsername] = useState('');
const [loginPassword, setLoginPassword] = useState('');
const [loginMode, setLoginMode] = useState<'password' | 'apikey'>('password');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Redirect if already logged in
React.useEffect(() => {
if (user) {
navigate('/');
}
}, [user, navigate]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
if (loginMode === 'apikey') {
if (!apiKeyInput.trim()) {
setError('API Key is required');
setIsLoading(false);
return;
}
// Verify API key is valid by calling the profile endpoint
const res = await fetch('/api/users/me', {
headers: { 'x-api-key': apiKeyInput }
});
if (res.ok) {
const userData = await res.json();
login(apiKeyInput, { ...userData, role: userData.role ?? 'USER' });
} else {
setError('Invalid API Key');
}
} else {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: loginUsername, password: loginPassword })
});
if (res.ok) {
const data = await res.json();
// data has { access_token, user }
// Get the API key using the JWT token
const keyRes = await fetch('/api/users/api-key', {
headers: { 'Authorization': `Bearer ${data.access_token}` }
});
if (keyRes.ok) {
const keyData = await keyRes.json();
login(keyData.apiKey, { ...data.user, role: data.user.role ?? 'USER' });
navigate('/');
} else {
// Fall back to using JWT access token as the key
login(data.access_token, { ...data.user, role: data.user.role ?? 'USER' });
navigate('/');
}
} else {
setError('Invalid username or password');
}
}
} catch (err) {
setError('An error occurred during login. Please try again.');
console.error(err);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-slate-50">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md p-8 bg-white border border-slate-200 shadow-xl rounded-2xl"
>
<div className="flex flex-col items-center mb-8">
<div className="p-3 mb-4 bg-blue-600 rounded-xl">
<BookOpen className="text-white" size={32} />
</div>
<h1 className="text-2xl font-bold text-slate-900">AuraK V2</h1>
<p className="text-slate-500">Sign in to your workspace</p>
</div>
<div className="flex gap-2 p-1 mb-6 bg-slate-100 rounded-lg">
<button
type="button"
onClick={() => { setLoginMode('password'); setError(''); }}
className={cn(
"flex-1 py-1.5 text-sm font-medium rounded-md transition-all",
loginMode === 'password' ? "bg-white shadow-sm text-blue-600" : "text-slate-500 hover:text-slate-700"
)}
>
Password
</button>
<button
type="button"
onClick={() => { setLoginMode('apikey'); setError(''); }}
className={cn(
"flex-1 py-1.5 text-sm font-medium rounded-md transition-all",
loginMode === 'apikey' ? "bg-white shadow-sm text-blue-600" : "text-slate-500 hover:text-slate-700"
)}
>
API Key
</button>
</div>
{error && (
<div className="mb-4 p-3 text-sm text-red-600 bg-red-50 border border-red-100 rounded-lg">
{error}
</div>
)}
<form onSubmit={handleLogin} className="space-y-4">
{loginMode === 'password' ? (
<>
<div>
<label className="block mb-1 text-sm font-medium text-slate-700">Username</label>
<input
type="text"
value={loginUsername}
onChange={(e) => setLoginUsername(e.target.value)}
placeholder="admin"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
required
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-slate-700">Password</label>
<input
type="password"
value={loginPassword}
onChange={(e) => setLoginPassword(e.target.value)}
placeholder="••••••••"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
required
/>
</div>
</>
) : (
<div>
<label className="block mb-1 text-sm font-medium text-slate-700">API Key</label>
<input
type="password"
value={apiKeyInput}
onChange={(e) => setApiKeyInput(e.target.value)}
placeholder="sk-..."
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
required
/>
</div>
)}
<button
type="submit"
disabled={isLoading}
className="w-full py-2.5 mt-2 text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-70 disabled:cursor-not-allowed rounded-lg font-semibold transition-colors flex items-center justify-center"
>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
</motion.div>
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import { AgentsView } from '../../components/views/AgentsView';
const AgentsPage: React.FC = () => {
return (
<div className="flex flex-col h-full">
<AgentsView />
</div>
);
};
export default AgentsPage;
@@ -0,0 +1,15 @@
import React from 'react';
import { AssessmentView } from '../../../components/views/AssessmentView';
import { useAuth } from '../../contexts/AuthContext';
export default function AssessmentPage() {
const { apiKey, logout, user } = useAuth();
return (
<AssessmentView
onLogout={logout}
onNavigate={() => { }}
isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
/>
);
}
+36
View File
@@ -0,0 +1,36 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { ChatView } from '../../../components/views/ChatView';
import { ModelConfig, DEFAULT_MODELS } from '../../../types';
import { modelConfigService } from '../../../services/modelConfigService';
export default function ChatPage() {
const { apiKey, logout, user } = useAuth();
const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>(DEFAULT_MODELS);
const fetchModels = useCallback(async () => {
if (!apiKey) return;
try {
const backendModels = await modelConfigService.getAll(apiKey);
const map = new Map<string, ModelConfig>();
DEFAULT_MODELS.forEach(m => map.set(m.id, m));
backendModels.forEach(m => map.set(m.id, m));
setModelConfigs(Array.from(map.values()));
} catch {
setModelConfigs(DEFAULT_MODELS);
}
}, [apiKey]);
useEffect(() => { fetchModels(); }, [fetchModels]);
return (
<ChatView
authToken={apiKey}
onLogout={logout}
modelConfigs={modelConfigs}
onNavigate={() => { }}
isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
/>
);
}
+35
View File
@@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { KnowledgeBaseView } from '../../../components/views/KnowledgeBaseView';
import { modelConfigService } from '../../../services/modelConfigService';
import { ModelConfig } from '../../../types';
export default function KnowledgePage() {
const { apiKey, user, logout, activeTenant } = useAuth();
const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>([]);
useEffect(() => {
const fetchModels = async () => {
if (!apiKey) return;
try {
const models = await modelConfigService.getAll(apiKey);
setModelConfigs(models);
} catch (error) {
console.error('Failed to fetch model configs:', error);
}
};
fetchModels();
}, [apiKey]);
const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
return (
<KnowledgeBaseView
authToken={apiKey || ''}
onLogout={logout}
onNavigate={() => { }}
modelConfigs={modelConfigs}
isAdmin={isAdmin}
/>
);
}
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { MemosView } from '../../../components/views/MemosView';
export default function MemosPage() {
const { apiKey, user, activeTenant } = useAuth();
const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
return (
<MemosView
authToken={apiKey}
isAdmin={isAdmin}
/>
);
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { NotebooksView } from '../../../components/views/NotebooksView';
const NotebooksPage: React.FC = () => {
const { apiKey, user, activeTenant } = useAuth()
const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
return (
<div className="flex flex-col h-full">
<NotebooksView
authToken={apiKey}
onChatWithContext={() => { }}
isAdmin={isAdmin}
/>
</div>
)
}
export default NotebooksPage;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import { PluginsView } from '../../components/views/PluginsView';
const PluginsPage: React.FC = () => {
return (
<div className="flex flex-col h-full">
<PluginsView />
</div>
);
};
export default PluginsPage;
+49
View File
@@ -0,0 +1,49 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { SettingsView } from '../../../components/views/SettingsView';
import { ModelConfig, DEFAULT_MODELS } from '../../../types';
import { modelConfigService } from '../../../services/modelConfigService';
interface SettingsPageProps {
initialTab?: 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base';
}
export default function SettingsPage({ initialTab }: SettingsPageProps) {
const { apiKey, user, activeTenant } = useAuth();
const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>(DEFAULT_MODELS);
const fetchModels = useCallback(async () => {
if (!apiKey) return;
try {
const backendModels = await modelConfigService.getAll(apiKey);
const map = new Map<string, ModelConfig>();
DEFAULT_MODELS.forEach(m => map.set(m.id, m));
backendModels.forEach(m => map.set(m.id, m));
setModelConfigs(Array.from(map.values()));
} catch {
setModelConfigs(DEFAULT_MODELS);
}
}, [apiKey]);
useEffect(() => { fetchModels(); }, [fetchModels]);
const handleUpdateModels = useCallback(async (action: 'create' | 'update' | 'delete', model: ModelConfig) => {
if (!apiKey) return;
const { id, ...data } = model;
if (action === 'create') await modelConfigService.create(apiKey, data);
else if (action === 'update') await modelConfigService.update(apiKey, id, data);
else if (action === 'delete') await modelConfigService.remove(apiKey, id);
await fetchModels();
}, [apiKey, fetchModels]);
return (
<SettingsView
models={modelConfigs}
onUpdateModels={handleUpdateModels}
authToken={apiKey}
isAdmin={user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN'}
currentUser={user}
initialTab={initialTab}
/>
);
}