mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
Added api caching capabilities - testing if changes break anything
This commit is contained in:
parent
fff082b4ee
commit
ddb718ff4c
8 changed files with 138 additions and 63 deletions
|
|
@ -169,7 +169,7 @@ const DashboardPage = () => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userData = await apiClient.get<User>("users/me");
|
const userData = await apiClient.get<User>('users/me', {}, 120);
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
setUserError(null);
|
setUserError(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@ export function AppSidebarProvider({
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const chats: Chat[] = await apiClient.get<Chat[]>(
|
const chats: Chat[] = await apiClient.get<Chat[]>(
|
||||||
`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`
|
`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`,
|
||||||
|
{},
|
||||||
|
15
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort chats by created_at in descending order (newest first)
|
// Sort chats by created_at in descending order (newest first)
|
||||||
|
|
@ -132,7 +134,9 @@ export function AppSidebarProvider({
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const data: SearchSpace = await apiClient.get<SearchSpace>(
|
const data: SearchSpace = await apiClient.get<SearchSpace>(
|
||||||
`api/v1/searchspaces/${searchSpaceId}`
|
`api/v1/searchspaces/${searchSpaceId}`,
|
||||||
|
{},
|
||||||
|
30
|
||||||
);
|
);
|
||||||
setSearchSpace(data);
|
setSearchSpace(data);
|
||||||
setSearchSpaceError(null);
|
setSearchSpaceError(null);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Message } from "@ai-sdk/react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import type { ResearchMode } from "@/components/chat";
|
import type { ResearchMode } from "@/components/chat";
|
||||||
import type { Document } from "@/hooks/use-documents";
|
import type { Document } from "@/hooks/use-documents";
|
||||||
|
import { fetchWithCache } from "@/lib/apiCache";
|
||||||
|
|
||||||
interface UseChatStateProps {
|
interface UseChatStateProps {
|
||||||
search_space_id: string;
|
search_space_id: string;
|
||||||
|
|
@ -53,7 +54,7 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) {
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const data = await fetchWithCache(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|
@ -61,14 +62,13 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
|
revalidate: 30
|
||||||
}
|
}
|
||||||
);
|
).catch(err => {
|
||||||
|
throw new Error(`Failed to fetch chat details: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
return await data;
|
||||||
throw new Error(`Failed to fetch chat details: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching chat details:", err);
|
console.error("Error fetching chat details:", err);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { fetchWithCache } from "@/lib/apiCache";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { normalizeListResponse } from "@/lib/pagination";
|
import { normalizeListResponse } from "@/lib/pagination";
|
||||||
|
|
||||||
|
|
@ -71,22 +72,19 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption
|
||||||
params.append("page_size", effectivePageSize.toString());
|
params.append("page_size", effectivePageSize.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const data = await fetchWithCache(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?${params.toString()}`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
},
|
},
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}
|
revalidate: 30
|
||||||
);
|
}
|
||||||
|
).catch(err => {
|
||||||
if (!response.ok) {
|
|
||||||
toast.error("Failed to fetch documents");
|
toast.error("Failed to fetch documents");
|
||||||
throw new Error("Failed to fetch documents");
|
throw new Error("Failed to fetch documents");
|
||||||
}
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const normalized = normalizeListResponse<Document>(data);
|
const normalized = normalizeListResponse<Document>(data);
|
||||||
setDocuments(normalized.items);
|
setDocuments(normalized.items);
|
||||||
setTotal(normalized.total);
|
setTotal(normalized.total);
|
||||||
|
|
@ -142,22 +140,19 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption
|
||||||
params.append("page_size", effectivePageSize.toString());
|
params.append("page_size", effectivePageSize.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const data = await fetchWithCache(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/search/?${params.toString()}`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/search/?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
},
|
},
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}
|
revalidate: 15
|
||||||
);
|
}
|
||||||
|
).catch(err => {
|
||||||
if (!response.ok) {
|
|
||||||
toast.error("Failed to search documents");
|
toast.error("Failed to search documents");
|
||||||
throw new Error("Failed to search documents");
|
throw new Error("Failed to search documents");
|
||||||
}
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const normalized = normalizeListResponse<Document>(data);
|
const normalized = normalizeListResponse<Document>(data);
|
||||||
setDocuments(normalized.items);
|
setDocuments(normalized.items);
|
||||||
setTotal(normalized.total);
|
setTotal(normalized.total);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { fetchWithCache } from "@/lib/apiCache";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export interface LLMConfig {
|
export interface LLMConfig {
|
||||||
|
|
@ -60,21 +61,19 @@ export function useLLMConfigs(searchSpaceId: number | null) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(
|
|
||||||
|
const data = await fetchWithCache(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/?search_space_id=${searchSpaceId}`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/?search_space_id=${searchSpaceId}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
},
|
},
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
revalidate: 60
|
||||||
}
|
}
|
||||||
);
|
).catch(err => {
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch LLM configurations");
|
throw new Error("Failed to fetch LLM configurations");
|
||||||
}
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setLlmConfigs(data);
|
setLlmConfigs(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -202,21 +201,19 @@ export function useLLMPreferences(searchSpaceId: number | null) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(
|
const data = await fetchWithCache(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
},
|
},
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
revalidate: 120
|
||||||
}
|
}
|
||||||
);
|
).catch(err => {
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch LLM preferences");
|
throw new Error("Failed to fetch LLM preferences");
|
||||||
}
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setPreferences(data);
|
setPreferences(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { fetchWithCache } from "@/lib/apiCache";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface SearchSpace {
|
interface SearchSpace {
|
||||||
|
|
@ -20,24 +21,20 @@ export function useSearchSpaces() {
|
||||||
const fetchSearchSpaces = async () => {
|
const fetchSearchSpaces = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(
|
|
||||||
|
const data = await fetchWithCache(
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/`,
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||||
},
|
},
|
||||||
method: "GET",
|
method: "GET",
|
||||||
cache: 'force-cache',
|
revalidate: 60
|
||||||
next: { revalidate: 60 }
|
|
||||||
}
|
}
|
||||||
);
|
).catch(err => {
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
toast.error("Not authenticated");
|
toast.error("Not authenticated");
|
||||||
throw new Error("Not authenticated");
|
throw new Error("Not authenticated");
|
||||||
}
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setSearchSpaces(data);
|
setSearchSpaces(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { fetchWithCache } from "./apiCache";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom fetch wrapper that handles authentication and redirects to home page on 401 Unauthorized
|
* Custom fetch wrapper that handles authentication and redirects to home page on 401 Unauthorized
|
||||||
|
|
@ -79,18 +80,49 @@ export const apiClient = {
|
||||||
* @param options - Additional fetch options
|
* @param options - Additional fetch options
|
||||||
* @returns The response data
|
* @returns The response data
|
||||||
*/
|
*/
|
||||||
async get<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async get<T>(
|
||||||
const response = await fetchWithAuth(getApiUrl(path), {
|
path: string,
|
||||||
|
options: RequestInit = {},
|
||||||
|
revalidate: number | false = 30 // Default 30 second cache
|
||||||
|
): Promise<T> {
|
||||||
|
const url = getApiUrl(path);
|
||||||
|
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => null);
|
const errorData = await response.json().catch(() => null);
|
||||||
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("surfsense_bearer_token");
|
||||||
|
const headers = {
|
||||||
|
...options.headers,
|
||||||
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return await fetchWithCache(url, {
|
||||||
|
method: "GET",
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
revalidate,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('401')) {
|
||||||
|
toast.error("Session expired. Please log in again.");
|
||||||
|
localStorage.removeItem("surfsense_bearer_token");
|
||||||
|
window.location.href = "/";
|
||||||
|
throw new Error("Unauthorized: Redirecting to login page");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
50
surfsense_web/lib/apiCache.ts
Normal file
50
surfsense_web/lib/apiCache.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
const NO_TRAILING_SLASH_ENDPOINTS = [
|
||||||
|
'users/me',
|
||||||
|
'api/v1/chats',
|
||||||
|
'api/v1/search-spaces'
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldSkipTrailingSlash(url: string): boolean {
|
||||||
|
const basePath = url.includes('?') ? url.split('?')[0] : url;
|
||||||
|
|
||||||
|
return NO_TRAILING_SLASH_ENDPOINTS.some(endpoint =>
|
||||||
|
basePath.includes(endpoint)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API client with Next.js caching and trailing slash handling
|
||||||
|
*/
|
||||||
|
export async function fetchWithCache(url: string, options: RequestInit & {
|
||||||
|
revalidate?: number | false,
|
||||||
|
} = {}) {
|
||||||
|
const { revalidate, ...fetchOptions } = options;
|
||||||
|
|
||||||
|
let basePath = url;
|
||||||
|
let queryParams = '';
|
||||||
|
|
||||||
|
if (url.includes('?')) {
|
||||||
|
[basePath, queryParams] = url.split('?');
|
||||||
|
queryParams = `?${queryParams}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldSkipTrailingSlash(basePath)) {
|
||||||
|
basePath = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
||||||
|
} else {
|
||||||
|
basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedUrl = basePath + queryParams;
|
||||||
|
|
||||||
|
const response = await fetch(formattedUrl, {
|
||||||
|
...fetchOptions,
|
||||||
|
cache: 'force-cache',
|
||||||
|
next: revalidate !== undefined ? { revalidate } : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue