Compare commits
No commits in common. "a7ad1e9bbac7d5b3caa72f7ae56367ff3ad525aa" and "68e5c0fac449954d7d05c080c83b5b69ee0564d7" have entirely different histories.
a7ad1e9bba
...
68e5c0fac4
10 changed files with 3 additions and 302 deletions
|
|
@ -42,10 +42,3 @@ AUTHENTIK_BOOTSTRAP_TOKEN=
|
||||||
# OPTIONAL - Logging
|
# OPTIONAL - Logging
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
LOG_REQUESTS=false
|
LOG_REQUESTS=false
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# OPTIONAL - System AI (Gemini API Key for server-side quiz generation)
|
|
||||||
# If set, users can generate quizzes using "System AI" without their own key.
|
|
||||||
# Get a key at: https://aistudio.google.com/apikey
|
|
||||||
# ==============================================================================
|
|
||||||
GEMINI_API_KEY=
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,6 @@ services:
|
||||||
OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/
|
OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/
|
||||||
CORS_ORIGIN: ${CORS_ORIGIN}
|
CORS_ORIGIN: ${CORS_ORIGIN}
|
||||||
LOG_REQUESTS: ${LOG_REQUESTS:-true}
|
LOG_REQUESTS: ${LOG_REQUESTS:-true}
|
||||||
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
|
|
||||||
volumes:
|
volumes:
|
||||||
- kaboot-data:/data
|
- kaboot-data:/data
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -615,7 +615,7 @@ export const useGame = () => {
|
||||||
questionCount?: number;
|
questionCount?: number;
|
||||||
files?: File[];
|
files?: File[];
|
||||||
useOcr?: boolean;
|
useOcr?: boolean;
|
||||||
aiProvider?: 'gemini' | 'openrouter' | 'openai' | 'system';
|
aiProvider?: 'gemini' | 'openrouter' | 'openai';
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
geminiModel?: string;
|
geminiModel?: string;
|
||||||
openRouterModel?: string;
|
openRouterModel?: string;
|
||||||
|
|
@ -642,7 +642,6 @@ export const useGame = () => {
|
||||||
geminiModel: options.geminiModel,
|
geminiModel: options.geminiModel,
|
||||||
openRouterModel: options.openRouterModel,
|
openRouterModel: options.openRouterModel,
|
||||||
openAIModel: options.openAIModel,
|
openAIModel: options.openAIModel,
|
||||||
accessToken: options.aiProvider === 'system' ? auth.user?.access_token : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generatedQuiz = await generateQuiz(generateOptions);
|
const generatedQuiz = await generateQuiz(generateOptions);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ print_header
|
||||||
|
|
||||||
KABOOT_DOMAIN=""
|
KABOOT_DOMAIN=""
|
||||||
AUTH_DOMAIN=""
|
AUTH_DOMAIN=""
|
||||||
GEMINI_API_KEY=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
|
@ -48,17 +47,12 @@ while [[ $# -gt 0 ]]; do
|
||||||
AUTH_DOMAIN="$2"
|
AUTH_DOMAIN="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--gemini-key)
|
|
||||||
GEMINI_API_KEY="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: $0 [OPTIONS]"
|
echo "Usage: $0 [OPTIONS]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --domain DOMAIN Main application domain (e.g., kaboot.example.com)"
|
echo " --domain DOMAIN Main application domain (e.g., kaboot.example.com)"
|
||||||
echo " --auth-domain DOMAIN Authentication domain (e.g., auth.example.com)"
|
echo " --auth-domain DOMAIN Authentication domain (e.g., auth.example.com)"
|
||||||
echo " --gemini-key KEY Gemini API key for system AI (optional)"
|
|
||||||
echo " --help, -h Show this help message"
|
echo " --help, -h Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "If options are not provided, you will be prompted for them."
|
echo "If options are not provided, you will be prompted for them."
|
||||||
|
|
@ -104,18 +98,6 @@ if [ -z "$AUTH_DOMAIN" ]; then
|
||||||
AUTH_DOMAIN=${AUTH_DOMAIN:-$DEFAULT_AUTH}
|
AUTH_DOMAIN=${AUTH_DOMAIN:-$DEFAULT_AUTH}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}System AI Configuration (Optional)${NC}"
|
|
||||||
echo "────────────────────────────────────────────────────────────"
|
|
||||||
echo "You can provide a Gemini API key to enable AI quiz generation"
|
|
||||||
echo "for all users without requiring them to set up their own key."
|
|
||||||
echo -e "${YELLOW}Note: This key will be embedded in the frontend and visible to users.${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ -z "$GEMINI_API_KEY" ]; then
|
|
||||||
read -p "Enter Gemini API key (or press Enter to skip): " GEMINI_API_KEY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
print_step "Generating secrets..."
|
print_step "Generating secrets..."
|
||||||
|
|
||||||
|
|
@ -155,9 +137,6 @@ OIDC_JWKS_URI=https://${AUTH_DOMAIN}/application/o/kaboot/jwks/
|
||||||
CORS_ORIGIN=https://${KABOOT_DOMAIN}
|
CORS_ORIGIN=https://${KABOOT_DOMAIN}
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
LOG_REQUESTS=true
|
LOG_REQUESTS=true
|
||||||
|
|
||||||
# System AI (optional - server-side quiz generation)
|
|
||||||
GEMINI_API_KEY=${GEMINI_API_KEY}
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
print_success "Created .env"
|
print_success "Created .env"
|
||||||
|
|
@ -216,18 +195,10 @@ fi
|
||||||
|
|
||||||
print_step "Building frontend with production URLs..."
|
print_step "Building frontend with production URLs..."
|
||||||
|
|
||||||
VITE_API_URL="https://${KABOOT_DOMAIN}" \
|
VITE_API_URL="https://${KABOOT_DOMAIN}/api" \
|
||||||
VITE_AUTHENTIK_URL="https://${AUTH_DOMAIN}" \
|
VITE_AUTHENTIK_URL="https://${AUTH_DOMAIN}" \
|
||||||
VITE_OIDC_CLIENT_ID="kaboot-spa" \
|
|
||||||
VITE_OIDC_APP_SLUG="kaboot" \
|
|
||||||
npm run build --silent 2>/dev/null || npm run build
|
npm run build --silent 2>/dev/null || npm run build
|
||||||
|
|
||||||
if [ -n "$GEMINI_API_KEY" ]; then
|
|
||||||
print_success "System AI will be available (key configured for backend)"
|
|
||||||
else
|
|
||||||
print_warning "No Gemini API key provided - users must configure their own"
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "Frontend built"
|
print_success "Frontend built"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -239,11 +210,6 @@ echo -e "${BOLD}Configuration Summary${NC}"
|
||||||
echo "────────────────────────────────────────────────────────────"
|
echo "────────────────────────────────────────────────────────────"
|
||||||
echo -e " Main Domain: ${BLUE}https://${KABOOT_DOMAIN}${NC}"
|
echo -e " Main Domain: ${BLUE}https://${KABOOT_DOMAIN}${NC}"
|
||||||
echo -e " Auth Domain: ${BLUE}https://${AUTH_DOMAIN}${NC}"
|
echo -e " Auth Domain: ${BLUE}https://${AUTH_DOMAIN}${NC}"
|
||||||
if [ -n "$GEMINI_API_KEY" ]; then
|
|
||||||
echo -e " System AI: ${GREEN}Enabled${NC}"
|
|
||||||
else
|
|
||||||
echo -e " System AI: ${YELLOW}Disabled (users need own API key)${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}Authentik Admin${NC}"
|
echo -e "${BOLD}Authentik Admin${NC}"
|
||||||
echo "────────────────────────────────────────────────────────────"
|
echo "────────────────────────────────────────────────────────────"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"test:get-token": "tsx --env-file=.env.test tests/get-token.ts"
|
"test:get-token": "tsx --env-file=.env.test tests/get-token.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^0.14.1",
|
|
||||||
"better-sqlite3": "^11.7.0",
|
"better-sqlite3": "^11.7.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import quizzesRouter from './routes/quizzes.js';
|
||||||
import usersRouter from './routes/users.js';
|
import usersRouter from './routes/users.js';
|
||||||
import uploadRouter from './routes/upload.js';
|
import uploadRouter from './routes/upload.js';
|
||||||
import gamesRouter from './routes/games.js';
|
import gamesRouter from './routes/games.js';
|
||||||
import generateRouter from './routes/generate.js';
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
@ -92,7 +91,6 @@ app.use('/api/quizzes', quizzesRouter);
|
||||||
app.use('/api/users', usersRouter);
|
app.use('/api/users', usersRouter);
|
||||||
app.use('/api/upload', uploadRouter);
|
app.use('/api/upload', uploadRouter);
|
||||||
app.use('/api/games', gamesRouter);
|
app.use('/api/games', gamesRouter);
|
||||||
app.use('/api/generate', generateRouter);
|
|
||||||
|
|
||||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
console.error('Unhandled error:', err);
|
console.error('Unhandled error:', err);
|
||||||
|
|
|
||||||
|
|
@ -82,23 +82,3 @@ export function requireAuth(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireAIAccess(
|
|
||||||
req: AuthenticatedRequest,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): void {
|
|
||||||
if (!req.user) {
|
|
||||||
res.status(401).json({ error: 'Authentication required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAccess = req.user.groups?.includes('kaboot-ai-access');
|
|
||||||
|
|
||||||
if (!hasAccess) {
|
|
||||||
res.status(403).json({ error: 'AI access not granted for this account' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
import { Router, Response } from 'express';
|
|
||||||
import { GoogleGenAI, Type, createUserContent, createPartFromUri } from '@google/genai';
|
|
||||||
import { requireAuth, AuthenticatedRequest, requireAIAccess } from '../middleware/auth.js';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
|
||||||
const DEFAULT_MODEL = 'gemini-2.5-flash-preview-05-20';
|
|
||||||
|
|
||||||
interface GenerateRequest {
|
|
||||||
topic: string;
|
|
||||||
questionCount?: number;
|
|
||||||
documents?: Array<{
|
|
||||||
type: 'text' | 'native';
|
|
||||||
content: string;
|
|
||||||
mimeType?: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUIZ_SCHEMA = {
|
|
||||||
type: Type.OBJECT,
|
|
||||||
properties: {
|
|
||||||
title: { type: Type.STRING, description: "A catchy title for the quiz" },
|
|
||||||
questions: {
|
|
||||||
type: Type.ARRAY,
|
|
||||||
items: {
|
|
||||||
type: Type.OBJECT,
|
|
||||||
properties: {
|
|
||||||
text: { type: Type.STRING, description: "The question text" },
|
|
||||||
options: {
|
|
||||||
type: Type.ARRAY,
|
|
||||||
items: {
|
|
||||||
type: Type.OBJECT,
|
|
||||||
properties: {
|
|
||||||
text: { type: Type.STRING },
|
|
||||||
isCorrect: { type: Type.BOOLEAN },
|
|
||||||
reason: { type: Type.STRING, description: "Brief explanation of why this answer is correct or incorrect" }
|
|
||||||
},
|
|
||||||
required: ["text", "isCorrect", "reason"]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["text", "options"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["title", "questions"]
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildPrompt(topic: string, questionCount: number, hasDocuments: boolean): string {
|
|
||||||
const baseInstructions = `Create ${questionCount} engaging multiple-choice questions. Each question must have exactly 4 options, and exactly one correct answer. Vary the difficulty.
|
|
||||||
|
|
||||||
IMPORTANT: For each option's reason, write as if you are directly explaining facts - never reference "the document", "the text", "the material", or "the source". Write explanations as standalone factual statements.`;
|
|
||||||
|
|
||||||
if (hasDocuments) {
|
|
||||||
const topicContext = topic ? ` Focus on aspects related to "${topic}".` : '';
|
|
||||||
return `Generate a quiz based on the provided content.${topicContext}\n\n${baseInstructions}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Generate a trivia quiz about "${topic}".\n\n${baseInstructions}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shuffleArray<T>(array: T[]): T[] {
|
|
||||||
const shuffled = [...array];
|
|
||||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
||||||
}
|
|
||||||
return shuffled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function transformToQuiz(data: any) {
|
|
||||||
const shapes = ['triangle', 'diamond', 'circle', 'square'] as const;
|
|
||||||
const colors = ['red', 'blue', 'yellow', 'green'] as const;
|
|
||||||
|
|
||||||
const questions = data.questions.map((q: any) => {
|
|
||||||
const shuffledOpts = shuffleArray(q.options);
|
|
||||||
|
|
||||||
const options = shuffledOpts.map((opt: any, index: number) => ({
|
|
||||||
text: opt.text,
|
|
||||||
isCorrect: opt.isCorrect,
|
|
||||||
shape: shapes[index % 4],
|
|
||||||
color: colors[index % 4],
|
|
||||||
reason: opt.reason
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: uuidv4(),
|
|
||||||
text: q.text,
|
|
||||||
options,
|
|
||||||
timeLimit: 20
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: data.title,
|
|
||||||
questions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/status', (_req, res: Response) => {
|
|
||||||
res.json({
|
|
||||||
available: !!GEMINI_API_KEY,
|
|
||||||
model: DEFAULT_MODEL
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', requireAuth, requireAIAccess, async (req: AuthenticatedRequest, res: Response) => {
|
|
||||||
if (!GEMINI_API_KEY) {
|
|
||||||
res.status(503).json({ error: 'System AI is not configured' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { topic, questionCount = 10, documents = [] } = req.body as GenerateRequest;
|
|
||||||
|
|
||||||
if (!topic && documents.length === 0) {
|
|
||||||
res.status(400).json({ error: 'Topic or documents required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
|
|
||||||
const hasDocuments = documents.length > 0;
|
|
||||||
const prompt = buildPrompt(topic, questionCount, hasDocuments);
|
|
||||||
|
|
||||||
let contents: any;
|
|
||||||
|
|
||||||
if (hasDocuments) {
|
|
||||||
const parts: any[] = [];
|
|
||||||
|
|
||||||
for (const doc of documents) {
|
|
||||||
if (doc.type === 'native' && doc.mimeType) {
|
|
||||||
const buffer = Buffer.from(doc.content, 'base64');
|
|
||||||
const blob = new Blob([buffer], { type: doc.mimeType });
|
|
||||||
|
|
||||||
const uploadedFile = await ai.files.upload({
|
|
||||||
file: blob,
|
|
||||||
config: { mimeType: doc.mimeType }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uploadedFile.uri && uploadedFile.mimeType) {
|
|
||||||
parts.push(createPartFromUri(uploadedFile.uri, uploadedFile.mimeType));
|
|
||||||
}
|
|
||||||
} else if (doc.type === 'text') {
|
|
||||||
parts.push({ text: doc.content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push({ text: prompt });
|
|
||||||
contents = createUserContent(parts);
|
|
||||||
} else {
|
|
||||||
contents = prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await ai.models.generateContent({
|
|
||||||
model: DEFAULT_MODEL,
|
|
||||||
contents,
|
|
||||||
config: {
|
|
||||||
responseMimeType: "application/json",
|
|
||||||
responseSchema: QUIZ_SCHEMA
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.text) {
|
|
||||||
res.status(500).json({ error: 'Failed to generate quiz content' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(response.text);
|
|
||||||
const quiz = transformToQuiz(data);
|
|
||||||
|
|
||||||
res.json(quiz);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('AI generation error:', err);
|
|
||||||
res.status(500).json({ error: err.message || 'Failed to generate quiz' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
@ -406,61 +406,9 @@ async function generateQuizWithOpenAI(options: GenerateQuizOptions): Promise<Qui
|
||||||
return transformToQuiz(data);
|
return transformToQuiz(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || '';
|
|
||||||
|
|
||||||
async function generateQuizWithServer(options: GenerateQuizOptions): Promise<Quiz> {
|
|
||||||
if (!options.accessToken) {
|
|
||||||
throw new Error("Authentication required for system AI");
|
|
||||||
}
|
|
||||||
|
|
||||||
const docs = options.documents || [];
|
|
||||||
const documentsPayload = docs.map(doc => ({
|
|
||||||
type: doc.type,
|
|
||||||
content: doc.type === 'native' && doc.content instanceof ArrayBuffer
|
|
||||||
? btoa(String.fromCharCode(...new Uint8Array(doc.content)))
|
|
||||||
: doc.content,
|
|
||||||
mimeType: doc.mimeType
|
|
||||||
}));
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/api/generate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${options.accessToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
topic: options.topic,
|
|
||||||
questionCount: options.questionCount || 10,
|
|
||||||
documents: documentsPayload
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
||||||
throw new Error(error.error || `Server error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkSystemAIAvailable(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/api/generate/status`);
|
|
||||||
if (!response.ok) return false;
|
|
||||||
const data = await response.json();
|
|
||||||
return data.available === true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz> => {
|
export const generateQuiz = async (options: GenerateQuizOptions): Promise<Quiz> => {
|
||||||
const provider = options.aiProvider || 'gemini';
|
const provider = options.aiProvider || 'gemini';
|
||||||
|
|
||||||
if (provider === 'system') {
|
|
||||||
return generateQuizWithServer(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'openrouter') {
|
if (provider === 'openrouter') {
|
||||||
return generateQuizWithOpenRouter(options);
|
return generateQuizWithOpenRouter(options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
types.ts
3
types.ts
|
|
@ -36,7 +36,7 @@ export const COLOR_SCHEMES: ColorScheme[] = [
|
||||||
{ id: 'rose', name: 'Rose', primary: '#e11d48', primaryDark: '#be123c', primaryDarker: '#5f1a2a' },
|
{ id: 'rose', name: 'Rose', primary: '#e11d48', primaryDark: '#be123c', primaryDarker: '#5f1a2a' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type AIProvider = 'gemini' | 'openrouter' | 'openai' | 'system';
|
export type AIProvider = 'gemini' | 'openrouter' | 'openai';
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
colorScheme: string;
|
colorScheme: string;
|
||||||
|
|
@ -140,7 +140,6 @@ export interface GenerateQuizOptions {
|
||||||
geminiModel?: string;
|
geminiModel?: string;
|
||||||
openRouterModel?: string;
|
openRouterModel?: string;
|
||||||
openAIModel?: string;
|
openAIModel?: string;
|
||||||
accessToken?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointsBreakdown {
|
export interface PointsBreakdown {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue