| import { AUTH_TOKEN_KEY } from './authClient'; |
|
|
| const getAuthHeaders = () => { |
| const token = localStorage.getItem(AUTH_TOKEN_KEY); |
| return { |
| 'Content-Type': 'application/json', |
| ...(token ? { Authorization: `Bearer ${token}` } : {}) |
| }; |
| }; |
|
|
| function apiUrl(path: string): string { |
| const base = ( |
| import.meta.env.VITE_API_BASE as string | undefined |
| ) |
| ?.trim() |
| .replace(/\/$/, '') ?? ''; |
| const p = path.startsWith('/') ? path : `/${path}`; |
| return `${base}${p}`; |
| } |
|
|
| export interface AIThread { |
| id: string; |
| userId: string; |
| title: string; |
| createdAt: string; |
| updatedAt: string; |
| } |
|
|
| export interface AIMessage { |
| id: string; |
| threadId: string; |
| role: 'user' | 'assistant' | 'system'; |
| content: string; |
| createdAt: string; |
| } |
|
|
| export type AITeacherMode = 'learning' | 'placement' | 'files'; |
|
|
| export async function fetchThreads(): Promise<AIThread[]> { |
| const res = await fetch(apiUrl('/api/ai/threads'), { |
| headers: getAuthHeaders() |
| }); |
| if (!res.ok) throw new Error('Failed to fetch threads'); |
| const data = await res.json(); |
| return data.threads; |
| } |
|
|
| export async function createThread(title: string): Promise<AIThread> { |
| const res = await fetch(apiUrl('/api/ai/threads/new'), { |
| method: 'POST', |
| headers: getAuthHeaders(), |
| body: JSON.stringify({ title }) |
| }); |
| if (!res.ok) throw new Error('Failed to create thread'); |
| const data = await res.json(); |
| return data.thread; |
| } |
|
|
| export async function fetchThreadMessages(threadId: string): Promise<AIMessage[]> { |
| const res = await fetch(apiUrl(`/api/ai/threads/messages?threadId=${threadId}`), { |
| headers: getAuthHeaders() |
| }); |
| if (!res.ok) throw new Error('Failed to fetch messages'); |
| const data = await res.json(); |
| return data.messages; |
| } |
|
|
| export async function deleteThread(threadId: string): Promise<void> { |
| const res = await fetch(apiUrl(`/api/ai/threads/delete?threadId=${threadId}`), { |
| method: 'DELETE', |
| headers: getAuthHeaders() |
| }); |
| if (!res.ok) throw new Error('Failed to delete thread'); |
| } |
|
|
| export interface AIFileAttachment { |
| name: string; |
| type: string; |
| size: number; |
| kind: string; |
| textExcerpt: string; |
| } |
|
|
| export interface AIChatResponse { |
| thread?: AIThread; |
| userMessage: AIMessage; |
| assistantMessage: AIMessage; |
| message: AIMessage; |
| } |
|
|
| export async function sendMessageToThread( |
| threadId: string, |
| message: string, |
| attachments?: AIFileAttachment[], |
| mode?: AITeacherMode, |
| ): Promise<AIChatResponse> { |
| const res = await fetch(apiUrl('/api/ai/teacher'), { |
| method: 'POST', |
| headers: getAuthHeaders(), |
| body: JSON.stringify({ |
| threadId, |
| message, |
| attachments: attachments || [], |
| mode: mode || 'learning', |
| }) |
| }); |
| if (!res.ok) { |
| let errorMessage = 'Failed to send message'; |
| try { |
| const data = await res.json(); |
| if (typeof data?.error === 'string' && data.error.trim()) { |
| errorMessage = data.error.trim(); |
| } |
| } catch { |
| |
| } |
| throw new Error(errorMessage); |
| } |
| const data = await res.json(); |
| return { |
| thread: data.thread, |
| userMessage: data.userMessage, |
| assistantMessage: data.assistantMessage || data.message, |
| message: data.message, |
| }; |
| } |
|
|