feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
interface AuthContextType {
isLoggedIn: boolean;
initialized: boolean;
login: (token: string, refreshToken?: string) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType>({
isLoggedIn: false,
initialized: false,
login: () => {},
logout: () => {},
});
export function AuthProvider({ children }: { children: ReactNode }) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
setIsLoggedIn(!!localStorage.getItem('token'));
setInitialized(true);
}, []);
const login = useCallback((token: string, refreshTk?: string) => {
localStorage.setItem('token', token);
if (refreshTk) localStorage.setItem('refreshToken', refreshTk);
setIsLoggedIn(true);
}, []);
const logout = useCallback(() => {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
setIsLoggedIn(false);
}, []);
return (
<AuthContext.Provider value={{ isLoggedIn, initialized, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
+90
View File
@@ -0,0 +1,90 @@
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
export function getToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('token');
}
export function getAdminToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('adminToken');
}
export function getRefreshToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('refreshToken');
}
export function setTokens(accessToken: string, refreshToken?: string) {
localStorage.setItem('token', accessToken);
if (refreshToken) localStorage.setItem('refreshToken', refreshToken);
}
export function clearTokens() {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
}
export function clearAdminToken() {
localStorage.removeItem('adminToken');
}
export function isLoggedIn(): boolean {
return !!getToken();
}
export function isAdminLoggedIn(): boolean {
return !!getAdminToken();
}
export async function adminApiFetch(url: string, opts: RequestInit = {}): Promise<Response> {
const token = getAdminToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(opts.headers as Record<string, string> || {}),
};
if (token) headers['Authorization'] = `Bearer ${token}`;
return fetch(`${API_BASE}${url}`, { ...opts, headers });
}
export async function refreshToken(): Promise<string | null> {
const token = getToken();
const refreshTk = getRefreshToken();
if (!token && !refreshTk) return null;
try {
const res = await fetch(`${API_BASE}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessToken: token }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);
setTokens(data.accessToken, data.refreshToken);
return data.accessToken;
} catch {
clearTokens();
return null;
}
}
export async function apiFetch(url: string, opts: RequestInit = {}): Promise<Response> {
const token = getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(opts.headers as Record<string, string> || {}),
};
if (token) headers['Authorization'] = `Bearer ${token}`;
let res = await fetch(`${API_BASE}${url}`, { ...opts, headers });
if (res.status === 401 && token) {
const newToken = await refreshToken();
if (newToken) {
headers['Authorization'] = `Bearer ${newToken}`;
res = await fetch(`${API_BASE}${url}`, { ...opts, headers });
}
}
return res;
}
+11
View File
@@ -0,0 +1,11 @@
export interface ModelOption {
id: string;
label: string;
}
export const AVAILABLE_MODELS: ModelOption[] = [
{ id: 'general', label: '通用模式' },
{ id: 'opencode-go', label: 'DeepSeek V4 Flash' },
];
export const DEFAULT_MODEL = 'general';
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}