mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-27 19:25:15 +02:00
Merge upstream/dev and accept upstream deletions
This commit is contained in:
commit
f05a313d73
260 changed files with 50971 additions and 36069 deletions
|
|
@ -18,7 +18,7 @@ class AuthApiService {
|
|||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ class AuthApiService {
|
|||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type z from "zod";
|
||||
import type { ZodType } from "zod";
|
||||
import { getBearerToken, handleUnauthorized } from "../auth-utils";
|
||||
import { AppError, AuthenticationError, AuthorizationError, NotFoundError } from "../error";
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ class BaseApiService {
|
|||
|
||||
async request<T, R extends ResponseType = ResponseType.JSON>(
|
||||
url: string,
|
||||
responseSchema?: z.ZodSchema<T>,
|
||||
responseSchema?: ZodType<T>,
|
||||
options?: RequestOptions & { responseType?: R }
|
||||
): Promise<
|
||||
R extends ResponseType.JSON
|
||||
|
|
@ -206,7 +206,7 @@ class BaseApiService {
|
|||
|
||||
async get<T>(
|
||||
url: string,
|
||||
responseSchema?: z.ZodSchema<T>,
|
||||
responseSchema?: ZodType<T>,
|
||||
options?: Omit<RequestOptions, "method" | "responseType">
|
||||
) {
|
||||
return this.request(url, responseSchema, {
|
||||
|
|
@ -221,7 +221,7 @@ class BaseApiService {
|
|||
|
||||
async post<T>(
|
||||
url: string,
|
||||
responseSchema?: z.ZodSchema<T>,
|
||||
responseSchema?: ZodType<T>,
|
||||
options?: Omit<RequestOptions, "method" | "responseType">
|
||||
) {
|
||||
return this.request(url, responseSchema, {
|
||||
|
|
@ -236,7 +236,7 @@ class BaseApiService {
|
|||
|
||||
async put<T>(
|
||||
url: string,
|
||||
responseSchema?: z.ZodSchema<T>,
|
||||
responseSchema?: ZodType<T>,
|
||||
options?: Omit<RequestOptions, "method" | "responseType">
|
||||
) {
|
||||
return this.request(url, responseSchema, {
|
||||
|
|
@ -251,7 +251,7 @@ class BaseApiService {
|
|||
|
||||
async delete<T>(
|
||||
url: string,
|
||||
responseSchema?: z.ZodSchema<T>,
|
||||
responseSchema?: ZodType<T>,
|
||||
options?: Omit<RequestOptions, "method" | "responseType">
|
||||
) {
|
||||
return this.request(url, responseSchema, {
|
||||
|
|
@ -274,7 +274,7 @@ class BaseApiService {
|
|||
|
||||
async postFormData<T>(
|
||||
url: string,
|
||||
responseSchema?: z.ZodSchema<T>,
|
||||
responseSchema?: ZodType<T>,
|
||||
options?: Omit<RequestOptions, "method" | "responseType" | "body"> & { body: FormData }
|
||||
) {
|
||||
// Remove Content-Type from options headers if present
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
type CreateChatRequest,
|
||||
chatDetails,
|
||||
chatSummary,
|
||||
createChatRequest,
|
||||
type DeleteChatRequest,
|
||||
deleteChatRequest,
|
||||
deleteChatResponse,
|
||||
type GetChatDetailsRequest,
|
||||
type GetChatsRequest,
|
||||
getChatDetailsRequest,
|
||||
getChatsRequest,
|
||||
type UpdateChatRequest,
|
||||
updateChatRequest,
|
||||
} from "@/contracts/types/chat.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class ChatApiService {
|
||||
getChatDetails = async (request: GetChatDetailsRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = getChatDetailsRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(`/api/v1/chats/${request.id}`, chatDetails);
|
||||
};
|
||||
|
||||
getChats = async (request: GetChatsRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = getChatsRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Transform queries params to be string values
|
||||
const transformedQueryParams = parsedRequest.data.queryParams
|
||||
? Object.fromEntries(
|
||||
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const queryParams = transformedQueryParams
|
||||
? new URLSearchParams(transformedQueryParams).toString()
|
||||
: undefined;
|
||||
|
||||
return baseApiService.get(`/api/v1/chats?${queryParams}`, z.array(chatSummary));
|
||||
};
|
||||
|
||||
deleteChat = async (request: DeleteChatRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = deleteChatRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(`/api/v1/chats/${request.id}`, deleteChatResponse);
|
||||
};
|
||||
|
||||
createChat = async (request: CreateChatRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = createChatRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(
|
||||
`/api/v1/chats`,
|
||||
|
||||
chatSummary,
|
||||
{
|
||||
body: parsedRequest.data,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
updateChat = async (request: UpdateChatRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = updateChatRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { type, title, initial_connectors, messages, search_space_id, id } = parsedRequest.data;
|
||||
|
||||
return baseApiService.put(
|
||||
`/api/v1/chats/${id}`,
|
||||
|
||||
chatSummary,
|
||||
{
|
||||
body: {
|
||||
type,
|
||||
title,
|
||||
initial_connectors,
|
||||
messages,
|
||||
search_space_id,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const chatsApiService = new ChatApiService();
|
||||
|
|
@ -40,7 +40,7 @@ class DocumentsApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ class DocumentsApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ class DocumentsApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ class DocumentsApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ class DocumentsApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ class DocumentsApiService {
|
|||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user friendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ class DocumentsApiService {
|
|||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user friendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ class DocumentsApiService {
|
|||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user friendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ class DocumentsApiService {
|
|||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user friendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
151
surfsense_web/lib/apis/invites-api.service.ts
Normal file
151
surfsense_web/lib/apis/invites-api.service.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import {
|
||||
type AcceptInviteRequest,
|
||||
type AcceptInviteResponse,
|
||||
acceptInviteRequest,
|
||||
acceptInviteResponse,
|
||||
type CreateInviteRequest,
|
||||
type CreateInviteResponse,
|
||||
createInviteRequest,
|
||||
createInviteResponse,
|
||||
type DeleteInviteRequest,
|
||||
type DeleteInviteResponse,
|
||||
deleteInviteRequest,
|
||||
deleteInviteResponse,
|
||||
type GetInviteInfoRequest,
|
||||
type GetInviteInfoResponse,
|
||||
type GetInvitesRequest,
|
||||
type GetInvitesResponse,
|
||||
getInviteInfoRequest,
|
||||
getInviteInfoResponse,
|
||||
getInvitesRequest,
|
||||
getInvitesResponse,
|
||||
type UpdateInviteRequest,
|
||||
type UpdateInviteResponse,
|
||||
updateInviteRequest,
|
||||
updateInviteResponse,
|
||||
} from "@/contracts/types/invites.types";
|
||||
import { ValidationError } from "@/lib/error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class InvitesApiService {
|
||||
/**
|
||||
* Create a new invite
|
||||
*/
|
||||
createInvite = async (request: CreateInviteRequest) => {
|
||||
const parsedRequest = createInviteRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/invites`,
|
||||
createInviteResponse,
|
||||
{
|
||||
body: parsedRequest.data.data,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all invites for a search space
|
||||
*/
|
||||
getInvites = async (request: GetInvitesRequest) => {
|
||||
const parsedRequest = getInvitesRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/invites`,
|
||||
getInvitesResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an invite
|
||||
*/
|
||||
updateInvite = async (request: UpdateInviteRequest) => {
|
||||
const parsedRequest = updateInviteRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.put(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/invites/${parsedRequest.data.invite_id}`,
|
||||
updateInviteResponse,
|
||||
{
|
||||
body: parsedRequest.data.data,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an invite
|
||||
*/
|
||||
deleteInvite = async (request: DeleteInviteRequest) => {
|
||||
const parsedRequest = deleteInviteRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/invites/${parsedRequest.data.invite_id}`,
|
||||
deleteInviteResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get invite info by invite code
|
||||
*/
|
||||
getInviteInfo = async (request: GetInviteInfoRequest) => {
|
||||
const parsedRequest = getInviteInfoRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/invites/${parsedRequest.data.invite_code}/info`,
|
||||
getInviteInfoResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept an invite
|
||||
*/
|
||||
acceptInvite = async (request: AcceptInviteRequest) => {
|
||||
const parsedRequest = acceptInviteRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(`/api/v1/invites/accept`, acceptInviteResponse, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const invitesApiService = new InvitesApiService();
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
import {
|
||||
type CreateLLMConfigRequest,
|
||||
createLLMConfigRequest,
|
||||
createLLMConfigResponse,
|
||||
type DeleteLLMConfigRequest,
|
||||
deleteLLMConfigRequest,
|
||||
deleteLLMConfigResponse,
|
||||
type GetLLMConfigRequest,
|
||||
type GetLLMConfigsRequest,
|
||||
type GetLLMPreferencesRequest,
|
||||
getGlobalLLMConfigsResponse,
|
||||
getLLMConfigRequest,
|
||||
getLLMConfigResponse,
|
||||
getLLMConfigsRequest,
|
||||
getLLMConfigsResponse,
|
||||
getLLMPreferencesRequest,
|
||||
getLLMPreferencesResponse,
|
||||
type UpdateLLMConfigRequest,
|
||||
type UpdateLLMPreferencesRequest,
|
||||
updateLLMConfigRequest,
|
||||
updateLLMConfigResponse,
|
||||
updateLLMPreferencesRequest,
|
||||
updateLLMPreferencesResponse,
|
||||
} from "@/contracts/types/llm-config.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class LLMConfigApiService {
|
||||
/**
|
||||
* Get all global LLM configurations available to all users
|
||||
*/
|
||||
getGlobalLLMConfigs = async () => {
|
||||
return baseApiService.get(`/api/v1/global-llm-configs`, getGlobalLLMConfigsResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new LLM configuration for a search space
|
||||
*/
|
||||
createLLMConfig = async (request: CreateLLMConfigRequest) => {
|
||||
const parsedRequest = createLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(`/api/v1/llm-configs`, createLLMConfigResponse, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of LLM configurations for a search space
|
||||
*/
|
||||
getLLMConfigs = async (request: GetLLMConfigsRequest) => {
|
||||
const parsedRequest = getLLMConfigsRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Transform query params to be string values
|
||||
const transformedQueryParams = parsedRequest.data.queryParams
|
||||
? Object.fromEntries(
|
||||
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => {
|
||||
return [k, String(v)];
|
||||
})
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const queryParams = transformedQueryParams
|
||||
? new URLSearchParams(transformedQueryParams).toString()
|
||||
: "";
|
||||
|
||||
return baseApiService.get(`/api/v1/llm-configs?${queryParams}`, getLLMConfigsResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single LLM configuration by ID
|
||||
*/
|
||||
getLLMConfig = async (request: GetLLMConfigRequest) => {
|
||||
const parsedRequest = getLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(`/api/v1/llm-configs/${request.id}`, getLLMConfigResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing LLM configuration
|
||||
*/
|
||||
updateLLMConfig = async (request: UpdateLLMConfigRequest) => {
|
||||
const parsedRequest = updateLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { id, data } = parsedRequest.data;
|
||||
|
||||
return baseApiService.put(`/api/v1/llm-configs/${id}`, updateLLMConfigResponse, {
|
||||
body: data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an LLM configuration
|
||||
*/
|
||||
deleteLLMConfig = async (request: DeleteLLMConfigRequest) => {
|
||||
const parsedRequest = deleteLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(`/api/v1/llm-configs/${request.id}`, deleteLLMConfigResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get LLM preferences for a search space
|
||||
*/
|
||||
getLLMPreferences = async (request: GetLLMPreferencesRequest) => {
|
||||
const parsedRequest = getLLMPreferencesRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/search-spaces/${request.search_space_id}/llm-preferences`,
|
||||
getLLMPreferencesResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update LLM preferences for a search space
|
||||
*/
|
||||
updateLLMPreferences = async (request: UpdateLLMPreferencesRequest) => {
|
||||
const parsedRequest = updateLLMPreferencesRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { search_space_id, data } = parsedRequest.data;
|
||||
|
||||
return baseApiService.put(
|
||||
`/api/v1/search-spaces/${search_space_id}/llm-preferences`,
|
||||
updateLLMPreferencesResponse,
|
||||
{
|
||||
body: data,
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const llmConfigApiService = new LLMConfigApiService();
|
||||
126
surfsense_web/lib/apis/members-api.service.ts
Normal file
126
surfsense_web/lib/apis/members-api.service.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import {
|
||||
type DeleteMembershipRequest,
|
||||
type DeleteMembershipResponse,
|
||||
deleteMembershipRequest,
|
||||
deleteMembershipResponse,
|
||||
type GetMembersRequest,
|
||||
type GetMembersResponse,
|
||||
type GetMyAccessRequest,
|
||||
type GetMyAccessResponse,
|
||||
getMembersRequest,
|
||||
getMembersResponse,
|
||||
getMyAccessRequest,
|
||||
getMyAccessResponse,
|
||||
type LeaveSearchSpaceRequest,
|
||||
type LeaveSearchSpaceResponse,
|
||||
leaveSearchSpaceRequest,
|
||||
leaveSearchSpaceResponse,
|
||||
type UpdateMembershipRequest,
|
||||
type UpdateMembershipResponse,
|
||||
updateMembershipRequest,
|
||||
updateMembershipResponse,
|
||||
} from "@/contracts/types/members.types";
|
||||
import { ValidationError } from "@/lib/error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class MembersApiService {
|
||||
/**
|
||||
* Get members of a search space
|
||||
*/
|
||||
getMembers = async (request: GetMembersRequest) => {
|
||||
const parsedRequest = getMembersRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/members`,
|
||||
getMembersResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a member's role
|
||||
*/
|
||||
updateMember = async (request: UpdateMembershipRequest) => {
|
||||
const parsedRequest = updateMembershipRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.put(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/members/${parsedRequest.data.membership_id}`,
|
||||
updateMembershipResponse,
|
||||
{
|
||||
body: parsedRequest.data.data,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a member from search space
|
||||
*/
|
||||
deleteMember = async (request: DeleteMembershipRequest) => {
|
||||
const parsedRequest = deleteMembershipRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/members/${parsedRequest.data.membership_id}`,
|
||||
deleteMembershipResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Leave a search space (remove self)
|
||||
*/
|
||||
leaveSearchSpace = async (request: LeaveSearchSpaceRequest) => {
|
||||
const parsedRequest = leaveSearchSpaceRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/members/me`,
|
||||
leaveSearchSpaceResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current user's access information for a search space
|
||||
*/
|
||||
getMyAccess = async (request: GetMyAccessRequest) => {
|
||||
const parsedRequest = getMyAccessRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/${parsedRequest.data.search_space_id}/my-access`,
|
||||
getMyAccessResponse
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const membersApiService = new MembersApiService();
|
||||
170
surfsense_web/lib/apis/new-llm-config-api.service.ts
Normal file
170
surfsense_web/lib/apis/new-llm-config-api.service.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import {
|
||||
type CreateNewLLMConfigRequest,
|
||||
createNewLLMConfigRequest,
|
||||
createNewLLMConfigResponse,
|
||||
type DeleteNewLLMConfigRequest,
|
||||
deleteNewLLMConfigRequest,
|
||||
deleteNewLLMConfigResponse,
|
||||
type GetNewLLMConfigRequest,
|
||||
type GetNewLLMConfigsRequest,
|
||||
getDefaultSystemInstructionsResponse,
|
||||
getGlobalNewLLMConfigsResponse,
|
||||
getLLMPreferencesResponse,
|
||||
getNewLLMConfigRequest,
|
||||
getNewLLMConfigResponse,
|
||||
getNewLLMConfigsRequest,
|
||||
getNewLLMConfigsResponse,
|
||||
type UpdateLLMPreferencesRequest,
|
||||
type UpdateNewLLMConfigRequest,
|
||||
updateLLMPreferencesRequest,
|
||||
updateLLMPreferencesResponse,
|
||||
updateNewLLMConfigRequest,
|
||||
updateNewLLMConfigResponse,
|
||||
} from "@/contracts/types/new-llm-config.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class NewLLMConfigApiService {
|
||||
/**
|
||||
* Get all global NewLLMConfigs available to all users
|
||||
*/
|
||||
getGlobalConfigs = async () => {
|
||||
return baseApiService.get(`/api/v1/global-new-llm-configs`, getGlobalNewLLMConfigsResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get default system instructions template
|
||||
*/
|
||||
getDefaultSystemInstructions = async () => {
|
||||
return baseApiService.get(
|
||||
`/api/v1/new-llm-configs/default-system-instructions`,
|
||||
getDefaultSystemInstructionsResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new NewLLMConfig for a search space
|
||||
*/
|
||||
createConfig = async (request: CreateNewLLMConfigRequest) => {
|
||||
const parsedRequest = createNewLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(`/api/v1/new-llm-configs`, createNewLLMConfigResponse, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of NewLLMConfigs for a search space
|
||||
*/
|
||||
getConfigs = async (request: GetNewLLMConfigsRequest) => {
|
||||
const parsedRequest = getNewLLMConfigsRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
search_space_id: String(parsedRequest.data.search_space_id),
|
||||
...(parsedRequest.data.skip !== undefined && { skip: String(parsedRequest.data.skip) }),
|
||||
...(parsedRequest.data.limit !== undefined && { limit: String(parsedRequest.data.limit) }),
|
||||
}).toString();
|
||||
|
||||
return baseApiService.get(`/api/v1/new-llm-configs?${queryParams}`, getNewLLMConfigsResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single NewLLMConfig by ID
|
||||
*/
|
||||
getConfig = async (request: GetNewLLMConfigRequest) => {
|
||||
const parsedRequest = getNewLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/new-llm-configs/${parsedRequest.data.id}`,
|
||||
getNewLLMConfigResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing NewLLMConfig
|
||||
*/
|
||||
updateConfig = async (request: UpdateNewLLMConfigRequest) => {
|
||||
const parsedRequest = updateNewLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { id, data } = parsedRequest.data;
|
||||
|
||||
return baseApiService.put(`/api/v1/new-llm-configs/${id}`, updateNewLLMConfigResponse, {
|
||||
body: data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a NewLLMConfig
|
||||
*/
|
||||
deleteConfig = async (request: DeleteNewLLMConfigRequest) => {
|
||||
const parsedRequest = deleteNewLLMConfigRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(
|
||||
`/api/v1/new-llm-configs/${parsedRequest.data.id}`,
|
||||
deleteNewLLMConfigResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get LLM preferences for a search space
|
||||
*/
|
||||
getLLMPreferences = async (searchSpaceId: number) => {
|
||||
return baseApiService.get(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/llm-preferences`,
|
||||
getLLMPreferencesResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update LLM preferences for a search space
|
||||
*/
|
||||
updateLLMPreferences = async (request: UpdateLLMPreferencesRequest) => {
|
||||
const parsedRequest = updateLLMPreferencesRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { search_space_id, data } = parsedRequest.data;
|
||||
|
||||
return baseApiService.put(
|
||||
`/api/v1/search-spaces/${search_space_id}/llm-preferences`,
|
||||
updateLLMPreferencesResponse,
|
||||
{ body: data }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const newLLMConfigApiService = new NewLLMConfigApiService();
|
||||
|
|
@ -16,7 +16,7 @@ const createNoteResponse = z.object({
|
|||
content: z.string(),
|
||||
content_hash: z.string(),
|
||||
unique_identifier_hash: z.string().nullable(),
|
||||
document_metadata: z.record(z.any()).nullable(),
|
||||
document_metadata: z.record(z.string(), z.any()).nullable(),
|
||||
search_space_id: z.number(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
|
|
@ -36,7 +36,7 @@ const noteItem = z.object({
|
|||
content: z.string(),
|
||||
content_hash: z.string(),
|
||||
unique_identifier_hash: z.string().nullable(),
|
||||
document_metadata: z.record(z.any()).nullable(),
|
||||
document_metadata: z.record(z.string(), z.any()).nullable(),
|
||||
search_space_id: z.number(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
|
|
@ -78,7 +78,7 @@ class NotesApiService {
|
|||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ class NotesApiService {
|
|||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ class NotesApiService {
|
|||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
import z from "zod";
|
||||
import {
|
||||
type DeletePodcastRequest,
|
||||
deletePodcastRequest,
|
||||
deletePodcastResponse,
|
||||
type GeneratePodcastRequest,
|
||||
type GetPodcastByChatIdRequest,
|
||||
type GetPodcastsRequest,
|
||||
generatePodcastRequest,
|
||||
getPodcastByChaIdResponse,
|
||||
getPodcastByChatIdRequest,
|
||||
getPodcastsRequest,
|
||||
type LoadPodcastRequest,
|
||||
loadPodcastRequest,
|
||||
podcast,
|
||||
} from "@/contracts/types/podcast.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class PodcastsApiService {
|
||||
getPodcasts = async (request: GetPodcastsRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = getPodcastsRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Transform queries params to be string values
|
||||
const transformedQueryParams = parsedRequest.data.queryParams
|
||||
? Object.fromEntries(
|
||||
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const queryParams = transformedQueryParams
|
||||
? new URLSearchParams(transformedQueryParams).toString()
|
||||
: undefined;
|
||||
|
||||
return baseApiService.get(`/api/v1/podcasts?${queryParams}`, z.array(podcast));
|
||||
};
|
||||
|
||||
getPodcastByChatId = async (request: GetPodcastByChatIdRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = getPodcastByChatIdRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(
|
||||
`/api/v1/podcasts/by-chat/${request.chat_id}`,
|
||||
getPodcastByChaIdResponse
|
||||
);
|
||||
};
|
||||
|
||||
generatePodcast = async (request: GeneratePodcastRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = generatePodcastRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(`/api/v1/podcasts/generate`, undefined, {
|
||||
body: parsedRequest.data,
|
||||
});
|
||||
};
|
||||
|
||||
loadPodcast = async ({
|
||||
request,
|
||||
controller,
|
||||
}: {
|
||||
request: LoadPodcastRequest;
|
||||
controller?: AbortController;
|
||||
}) => {
|
||||
// Validate the request
|
||||
const parsedRequest = loadPodcastRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return await baseApiService.getBlob(`/api/v1/podcasts/${request.id}/stream`, {
|
||||
signal: controller?.signal,
|
||||
});
|
||||
};
|
||||
|
||||
deletePodcast = async (request: DeletePodcastRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = deletePodcastRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
// Format a user frendly error message
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.delete(`/api/v1/podcasts/${request.id}`, deletePodcastResponse);
|
||||
};
|
||||
}
|
||||
|
||||
export const podcastsApiService = new PodcastsApiService();
|
||||
|
|
@ -25,7 +25,7 @@ class RolesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ class RolesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ class RolesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ class RolesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ class RolesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
deleteSearchSpaceResponse,
|
||||
type GetSearchSpaceRequest,
|
||||
type GetSearchSpacesRequest,
|
||||
getCommunityPromptsResponse,
|
||||
getSearchSpaceRequest,
|
||||
getSearchSpaceResponse,
|
||||
getSearchSpacesRequest,
|
||||
|
|
@ -29,7 +28,7 @@ class SearchSpacesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ class SearchSpacesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -67,16 +66,6 @@ class SearchSpacesApiService {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get community-curated prompts for search space system instructions
|
||||
*/
|
||||
getCommunityPrompts = async () => {
|
||||
return baseApiService.get(
|
||||
`/api/v1/searchspaces/prompts/community`,
|
||||
getCommunityPromptsResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single search space by ID
|
||||
*/
|
||||
|
|
@ -86,7 +75,7 @@ class SearchSpacesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +91,7 @@ class SearchSpacesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +109,7 @@ class SearchSpacesApiService {
|
|||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
||||
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
324
surfsense_web/lib/chat/attachment-adapter.ts
Normal file
324
surfsense_web/lib/chat/attachment-adapter.ts
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
/**
|
||||
* Attachment adapter for assistant-ui
|
||||
*
|
||||
* This adapter handles file uploads by:
|
||||
* 1. Uploading the file to the backend /attachments/process endpoint
|
||||
* 2. The backend extracts markdown content using the configured ETL service
|
||||
* 3. The extracted content is stored in the attachment and sent with messages
|
||||
*/
|
||||
|
||||
import type { AttachmentAdapter, CompleteAttachment, PendingAttachment } from "@assistant-ui/react";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
|
||||
/**
|
||||
* Supported file types for the attachment adapter
|
||||
*
|
||||
* - Text/Markdown: .md, .markdown, .txt
|
||||
* - Audio (if STT configured): .mp3, .mp4, .mpeg, .mpga, .m4a, .wav, .webm
|
||||
* - Documents (depends on ETL service): .pdf, .docx, .doc, .pptx, .xlsx, .html
|
||||
* - Images: .jpg, .jpeg, .png, .gif, .webp
|
||||
*/
|
||||
const ACCEPTED_FILE_TYPES = [
|
||||
// Text/Markdown (always supported)
|
||||
".md",
|
||||
".markdown",
|
||||
".txt",
|
||||
// Audio files
|
||||
".mp3",
|
||||
".mp4",
|
||||
".mpeg",
|
||||
".mpga",
|
||||
".m4a",
|
||||
".wav",
|
||||
".webm",
|
||||
// Document files (depends on ETL service)
|
||||
".pdf",
|
||||
".docx",
|
||||
".doc",
|
||||
".pptx",
|
||||
".xlsx",
|
||||
".html",
|
||||
// Image files
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".webp",
|
||||
].join(",");
|
||||
|
||||
/**
|
||||
* Response from the attachment processing endpoint
|
||||
*/
|
||||
interface ProcessAttachmentResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "document" | "image" | "file";
|
||||
content: string;
|
||||
contentLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended CompleteAttachment with our custom extractedContent field
|
||||
* We store the extracted text in a custom field so we can access it in onNew
|
||||
* For images, we also store the data URL so it can be displayed after persistence
|
||||
*/
|
||||
export interface ChatAttachment extends CompleteAttachment {
|
||||
extractedContent: string;
|
||||
imageDataUrl?: string; // Base64 data URL for images (persists across page reloads)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a file through the backend ETL service
|
||||
*/
|
||||
async function processAttachment(file: File): Promise<ProcessAttachmentResponse> {
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch(`${backendUrl}/api/v1/attachments/process`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[processAttachment] Error response:", errorText);
|
||||
let errorDetail = "Unknown error";
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
// FastAPI validation errors return detail as array
|
||||
if (Array.isArray(errorJson.detail)) {
|
||||
errorDetail = errorJson.detail
|
||||
.map((err: { msg?: string; loc?: string[] }) => {
|
||||
const field = err.loc?.join(".") || "unknown";
|
||||
return `${field}: ${err.msg || "validation error"}`;
|
||||
})
|
||||
.join("; ");
|
||||
} else if (typeof errorJson.detail === "string") {
|
||||
errorDetail = errorJson.detail;
|
||||
} else {
|
||||
errorDetail = JSON.stringify(errorJson);
|
||||
}
|
||||
} catch {
|
||||
errorDetail = errorText || `HTTP ${response.status}`;
|
||||
}
|
||||
throw new Error(errorDetail);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Store processed results for the send() method
|
||||
const processedAttachments = new Map<string, ProcessAttachmentResponse>();
|
||||
|
||||
// Store image data URLs for attachments (so they persist after File objects are lost)
|
||||
const imageDataUrls = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Convert a File to a data URL (base64) for images
|
||||
*/
|
||||
async function fileToDataUrl(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the attachment adapter for assistant-ui
|
||||
*
|
||||
* This adapter:
|
||||
* 1. Accepts file upload
|
||||
* 2. Processes the file through the backend ETL service
|
||||
* 3. Returns the attachment with extracted markdown content
|
||||
*
|
||||
* The content is stored in the attachment and will be sent with the message.
|
||||
*/
|
||||
export function createAttachmentAdapter(): AttachmentAdapter {
|
||||
return {
|
||||
accept: ACCEPTED_FILE_TYPES,
|
||||
|
||||
/**
|
||||
* Async generator that yields pending states while processing
|
||||
* and returns a pending attachment when done.
|
||||
*
|
||||
* IMPORTANT: The generator should return status: { type: "running", progress: 100 }
|
||||
* NOT status: { type: "complete" }. The "complete" status is set by send().
|
||||
* Returning "complete" from the generator will prevent send() from being called!
|
||||
*
|
||||
* This pattern allows the UI to show a loading indicator
|
||||
* while the file is being processed by the backend.
|
||||
* The send() method is called to finalize the attachment.
|
||||
*/
|
||||
async *add(input: File | { file: File }): AsyncGenerator<PendingAttachment, void> {
|
||||
// Handle both direct File and { file: File } patterns
|
||||
const file = input instanceof File ? input : input.file;
|
||||
|
||||
if (!file) {
|
||||
console.error("[AttachmentAdapter] No file found in input:", input);
|
||||
throw new Error("No file provided");
|
||||
}
|
||||
|
||||
// Generate a unique ID for this attachment
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
// Determine attachment type from file
|
||||
const attachmentType = file.type.startsWith("image/") ? "image" : "document";
|
||||
|
||||
// Yield initial pending state with "running" status (0% progress)
|
||||
// This triggers the loading indicator in the UI
|
||||
yield {
|
||||
id,
|
||||
type: attachmentType,
|
||||
name: file.name,
|
||||
file,
|
||||
status: { type: "running", reason: "uploading", progress: 0 },
|
||||
} as PendingAttachment;
|
||||
|
||||
try {
|
||||
// For images, convert to data URL so we can display them after persistence
|
||||
if (attachmentType === "image") {
|
||||
const dataUrl = await fileToDataUrl(file);
|
||||
imageDataUrls.set(id, dataUrl);
|
||||
}
|
||||
|
||||
// Process the file through the backend ETL service
|
||||
const result = await processAttachment(file);
|
||||
|
||||
// Verify we have the required fields
|
||||
if (!result.content) {
|
||||
console.error("[AttachmentAdapter] WARNING: No content received from backend!");
|
||||
}
|
||||
|
||||
// Store the processed result for send()
|
||||
processedAttachments.set(id, result);
|
||||
|
||||
// Create the final pending attachment
|
||||
// IMPORTANT: Use "running" status with progress: 100 to indicate processing is done
|
||||
// but attachment is still pending. The "complete" status will be set by send().
|
||||
// Yield the final state to ensure it gets processed by the UI
|
||||
yield {
|
||||
id,
|
||||
type: result.type,
|
||||
name: result.name,
|
||||
file,
|
||||
status: { type: "running", reason: "uploading", progress: 100 },
|
||||
} as PendingAttachment;
|
||||
} catch (error) {
|
||||
console.error("[AttachmentAdapter] Failed to process attachment:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when user sends the message.
|
||||
* Converts the pending attachment to a complete attachment.
|
||||
*/
|
||||
async send(pendingAttachment: PendingAttachment): Promise<ChatAttachment> {
|
||||
const result = processedAttachments.get(pendingAttachment.id);
|
||||
const imageDataUrl = imageDataUrls.get(pendingAttachment.id);
|
||||
|
||||
if (result) {
|
||||
// Clean up stored result
|
||||
processedAttachments.delete(pendingAttachment.id);
|
||||
if (imageDataUrl) {
|
||||
imageDataUrls.delete(pendingAttachment.id);
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
type: result.type,
|
||||
name: result.name,
|
||||
contentType: "text/markdown",
|
||||
status: { type: "complete" },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: result.content,
|
||||
},
|
||||
],
|
||||
extractedContent: result.content,
|
||||
imageDataUrl, // Store data URL for images so they can be displayed after persistence
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback if no processed result found
|
||||
console.warn(
|
||||
"[AttachmentAdapter] send() - No processed result found for attachment:",
|
||||
pendingAttachment.id
|
||||
);
|
||||
return {
|
||||
id: pendingAttachment.id,
|
||||
type: pendingAttachment.type,
|
||||
name: pendingAttachment.name,
|
||||
contentType: "text/plain",
|
||||
status: { type: "complete" },
|
||||
content: [],
|
||||
extractedContent: "",
|
||||
imageDataUrl, // Still include data URL if available
|
||||
};
|
||||
},
|
||||
|
||||
async remove() {
|
||||
// No server-side cleanup needed since we don't persist attachments
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attachment content for chat request
|
||||
*
|
||||
* This function extracts the content from attachments to be sent with the chat request.
|
||||
* Only attachments that have been fully processed (have content) will be included.
|
||||
*/
|
||||
export function extractAttachmentContent(
|
||||
attachments: Array<unknown>
|
||||
): Array<{ id: string; name: string; type: string; content: string }> {
|
||||
return attachments
|
||||
.filter((att): att is ChatAttachment => {
|
||||
if (!att || typeof att !== "object") return false;
|
||||
const a = att as Record<string, unknown>;
|
||||
// Check for our custom extractedContent field first
|
||||
if (typeof a.extractedContent === "string" && a.extractedContent.length > 0) {
|
||||
return true;
|
||||
}
|
||||
// Fallback: check if content array has text content
|
||||
if (Array.isArray(a.content)) {
|
||||
const textContent = (a.content as Array<{ type: string; text?: string }>).find(
|
||||
(c) => c.type === "text" && typeof c.text === "string" && c.text.length > 0
|
||||
);
|
||||
return Boolean(textContent);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((att) => {
|
||||
// Get content from extractedContent or from content array
|
||||
let content = "";
|
||||
if (typeof att.extractedContent === "string") {
|
||||
content = att.extractedContent;
|
||||
} else if (Array.isArray(att.content)) {
|
||||
const textContent = (att.content as Array<{ type: string; text?: string }>).find(
|
||||
(c) => c.type === "text"
|
||||
);
|
||||
content = textContent?.text || "";
|
||||
}
|
||||
|
||||
return {
|
||||
id: att.id,
|
||||
name: att.name,
|
||||
type: att.type,
|
||||
content,
|
||||
};
|
||||
});
|
||||
}
|
||||
73
surfsense_web/lib/chat/podcast-state.ts
Normal file
73
surfsense_web/lib/chat/podcast-state.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Module-level state for tracking active podcast generation.
|
||||
* Used by the new-chat adapter to prevent duplicate podcast requests.
|
||||
*/
|
||||
|
||||
type PodcastStateListener = (isGenerating: boolean) => void;
|
||||
|
||||
let _activePodcastTaskId: string | null = null;
|
||||
const _listeners: Set<PodcastStateListener> = new Set();
|
||||
|
||||
/**
|
||||
* Check if a podcast is currently being generated
|
||||
*/
|
||||
export function isPodcastGenerating(): boolean {
|
||||
return _activePodcastTaskId !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active podcast task ID
|
||||
*/
|
||||
export function getActivePodcastTaskId(): string | null {
|
||||
return _activePodcastTaskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active podcast task ID (when podcast generation starts)
|
||||
*/
|
||||
export function setActivePodcastTaskId(taskId: string): void {
|
||||
_activePodcastTaskId = taskId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the active podcast task ID (when podcast generation completes or errors)
|
||||
*/
|
||||
export function clearActivePodcastTaskId(): void {
|
||||
_activePodcastTaskId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to podcast state changes
|
||||
*/
|
||||
export function subscribeToPodcastState(listener: PodcastStateListener): () => void {
|
||||
_listeners.add(listener);
|
||||
return () => {
|
||||
_listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function notifyListeners(): void {
|
||||
const isGenerating = _activePodcastTaskId !== null;
|
||||
for (const listener of _listeners) {
|
||||
listener(isGenerating);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message looks like a podcast request
|
||||
*/
|
||||
export function looksLikePodcastRequest(message: string): boolean {
|
||||
const podcastPatterns = [
|
||||
/\bpodcast\b/i,
|
||||
/\bcreate.*podcast\b/i,
|
||||
/\bgenerate.*podcast\b/i,
|
||||
/\bmake.*podcast\b/i,
|
||||
/\bturn.*into.*podcast\b/i,
|
||||
/\bpodcast.*about\b/i,
|
||||
/\bgive.*podcast\b/i,
|
||||
];
|
||||
|
||||
return podcastPatterns.some((pattern) => pattern.test(message));
|
||||
}
|
||||
229
surfsense_web/lib/chat/thread-persistence.ts
Normal file
229
surfsense_web/lib/chat/thread-persistence.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* Thread persistence utilities for the new chat feature.
|
||||
* Provides API functions and thread list management.
|
||||
*/
|
||||
|
||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
|
||||
// =============================================================================
|
||||
// Types matching backend schemas
|
||||
// =============================================================================
|
||||
|
||||
export interface ThreadRecord {
|
||||
id: number;
|
||||
title: string;
|
||||
archived: boolean;
|
||||
search_space_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MessageRecord {
|
||||
id: number;
|
||||
thread_id: number;
|
||||
role: "user" | "assistant" | "system";
|
||||
content: unknown;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ThreadListResponse {
|
||||
threads: ThreadListItem[];
|
||||
archived_threads: ThreadListItem[];
|
||||
}
|
||||
|
||||
export interface ThreadListItem {
|
||||
id: number;
|
||||
title: string;
|
||||
archived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ThreadHistoryLoadResponse {
|
||||
messages: MessageRecord[];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Service Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch list of threads for a search space
|
||||
*/
|
||||
export async function fetchThreads(
|
||||
searchSpaceId: number,
|
||||
limit?: number
|
||||
): Promise<ThreadListResponse> {
|
||||
const params = new URLSearchParams({ search_space_id: String(searchSpaceId) });
|
||||
if (limit) params.append("limit", String(limit));
|
||||
return baseApiService.get<ThreadListResponse>(`/api/v1/threads?${params}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search threads by title
|
||||
*/
|
||||
export async function searchThreads(
|
||||
searchSpaceId: number,
|
||||
title: string
|
||||
): Promise<ThreadListItem[]> {
|
||||
const params = new URLSearchParams({
|
||||
search_space_id: String(searchSpaceId),
|
||||
title,
|
||||
});
|
||||
return baseApiService.get<ThreadListItem[]>(`/api/v1/threads/search?${params}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new thread
|
||||
*/
|
||||
export async function createThread(
|
||||
searchSpaceId: number,
|
||||
title = "New Chat"
|
||||
): Promise<ThreadRecord> {
|
||||
return baseApiService.post<ThreadRecord>("/api/v1/threads", undefined, {
|
||||
body: {
|
||||
title,
|
||||
archived: false,
|
||||
search_space_id: searchSpaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get thread messages
|
||||
*/
|
||||
export async function getThreadMessages(threadId: number): Promise<ThreadHistoryLoadResponse> {
|
||||
return baseApiService.get<ThreadHistoryLoadResponse>(`/api/v1/threads/${threadId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a message to a thread
|
||||
*/
|
||||
export async function appendMessage(
|
||||
threadId: number,
|
||||
message: { role: "user" | "assistant" | "system"; content: unknown }
|
||||
): Promise<MessageRecord> {
|
||||
return baseApiService.post<MessageRecord>(`/api/v1/threads/${threadId}/messages`, undefined, {
|
||||
body: message,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update thread (rename, archive)
|
||||
*/
|
||||
export async function updateThread(
|
||||
threadId: number,
|
||||
updates: { title?: string; archived?: boolean }
|
||||
): Promise<ThreadRecord> {
|
||||
return baseApiService.put<ThreadRecord>(`/api/v1/threads/${threadId}`, undefined, {
|
||||
body: updates,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a thread
|
||||
*/
|
||||
export async function deleteThread(threadId: number): Promise<void> {
|
||||
await baseApiService.delete(`/api/v1/threads/${threadId}`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Thread List Manager (for thread list sidebar)
|
||||
// =============================================================================
|
||||
|
||||
export interface ThreadListAdapterConfig {
|
||||
searchSpaceId: number;
|
||||
currentThreadId: number | null;
|
||||
onThreadSwitch: (threadId: number) => void;
|
||||
onNewThread: (threadId: number) => void;
|
||||
}
|
||||
|
||||
export interface ThreadListState {
|
||||
threads: ThreadListItem[];
|
||||
archivedThreads: ThreadListItem[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a thread list management object.
|
||||
* This provides methods to manage the thread list for the sidebar.
|
||||
*/
|
||||
export function createThreadListManager(config: ThreadListAdapterConfig) {
|
||||
return {
|
||||
async loadThreads(): Promise<ThreadListState> {
|
||||
try {
|
||||
const response = await fetchThreads(config.searchSpaceId);
|
||||
return {
|
||||
threads: response.threads,
|
||||
archivedThreads: response.archived_threads,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[ThreadListManager] Failed to load threads:", error);
|
||||
return {
|
||||
threads: [],
|
||||
archivedThreads: [],
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : "Failed to load threads",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async createNewThread(title = "New Chat"): Promise<number | null> {
|
||||
try {
|
||||
const thread = await createThread(config.searchSpaceId, title);
|
||||
config.onNewThread(thread.id);
|
||||
return thread.id;
|
||||
} catch (error) {
|
||||
console.error("[ThreadListManager] Failed to create thread:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
switchToThread(threadId: number) {
|
||||
config.onThreadSwitch(threadId);
|
||||
},
|
||||
|
||||
async renameThread(threadId: number, newTitle: string): Promise<boolean> {
|
||||
try {
|
||||
await updateThread(threadId, { title: newTitle });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[ThreadListManager] Failed to rename thread:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async archiveThread(threadId: number): Promise<boolean> {
|
||||
try {
|
||||
await updateThread(threadId, { archived: true });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[ThreadListManager] Failed to archive thread:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async unarchiveThread(threadId: number): Promise<boolean> {
|
||||
try {
|
||||
await updateThread(threadId, { archived: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[ThreadListManager] Failed to unarchive thread:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteThread(threadId: number): Promise<boolean> {
|
||||
try {
|
||||
await deleteThread(threadId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[ThreadListManager] Failed to delete thread:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
// Helper function to get connector type display name
|
||||
export const getConnectorTypeDisplay = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
SERPER_API: "Serper API",
|
||||
TAVILY_API: "Tavily API",
|
||||
SEARXNG_API: "SearxNG",
|
||||
SLACK_CONNECTOR: "Slack",
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
// Helper to normalize list responses from the API
|
||||
// Supports shapes: Array<T>, { items: T[]; total: number }, and tuple [T[], total]
|
||||
export type ListResponse<T> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export function normalizeListResponse<T>(payload: any): ListResponse<T> {
|
||||
try {
|
||||
// Case 1: already in desired shape
|
||||
if (payload && Array.isArray(payload.items)) {
|
||||
const total = typeof payload.total === "number" ? payload.total : payload.items.length;
|
||||
return { items: payload.items as T[], total };
|
||||
}
|
||||
|
||||
// Case 2: tuple [items, total]
|
||||
if (Array.isArray(payload) && payload.length === 2 && Array.isArray(payload[0])) {
|
||||
const items = (payload[0] ?? []) as T[];
|
||||
const rawTotal = payload[1];
|
||||
const total = typeof rawTotal === "number" ? rawTotal : items.length;
|
||||
return { items, total };
|
||||
}
|
||||
|
||||
// Case 3: plain array
|
||||
if (Array.isArray(payload)) {
|
||||
return { items: payload as T[], total: (payload as T[]).length };
|
||||
}
|
||||
} catch (e) {
|
||||
// fallthrough to default
|
||||
}
|
||||
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
291
surfsense_web/lib/posthog/events.ts
Normal file
291
surfsense_web/lib/posthog/events.ts
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import posthog from "posthog-js";
|
||||
|
||||
/**
|
||||
* PostHog Analytics Event Definitions
|
||||
*
|
||||
* This file defines all custom analytics events tracked in SurfSense.
|
||||
* Events follow a consistent naming convention: category_action
|
||||
*
|
||||
* Categories:
|
||||
* - auth: Authentication events
|
||||
* - search_space: Search space management
|
||||
* - document: Document management
|
||||
* - chat: Chat and messaging
|
||||
* - connector: External connector events
|
||||
* - contact: Contact form events
|
||||
* - settings: Settings changes
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// AUTH EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackLoginAttempt(method: "local" | "google") {
|
||||
posthog.capture("auth_login_attempt", {
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackLoginSuccess(method: "local" | "google") {
|
||||
posthog.capture("auth_login_success", {
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackLoginFailure(method: "local" | "google", error?: string) {
|
||||
posthog.capture("auth_login_failure", {
|
||||
method,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackRegistrationAttempt() {
|
||||
posthog.capture("auth_registration_attempt");
|
||||
}
|
||||
|
||||
export function trackRegistrationSuccess() {
|
||||
posthog.capture("auth_registration_success");
|
||||
}
|
||||
|
||||
export function trackRegistrationFailure(error?: string) {
|
||||
posthog.capture("auth_registration_failure", {
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackLogout() {
|
||||
posthog.capture("auth_logout");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEARCH SPACE EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackSearchSpaceCreated(searchSpaceId: number, name: string) {
|
||||
posthog.capture("search_space_created", {
|
||||
search_space_id: searchSpaceId,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSearchSpaceDeleted(searchSpaceId: number) {
|
||||
posthog.capture("search_space_deleted", {
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSearchSpaceViewed(searchSpaceId: number) {
|
||||
posthog.capture("search_space_viewed", {
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CHAT EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackChatCreated(searchSpaceId: number, chatId: number) {
|
||||
posthog.capture("chat_created", {
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackChatMessageSent(
|
||||
searchSpaceId: number,
|
||||
chatId: number,
|
||||
options?: {
|
||||
hasAttachments?: boolean;
|
||||
hasMentionedDocuments?: boolean;
|
||||
messageLength?: number;
|
||||
}
|
||||
) {
|
||||
posthog.capture("chat_message_sent", {
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId,
|
||||
has_attachments: options?.hasAttachments ?? false,
|
||||
has_mentioned_documents: options?.hasMentionedDocuments ?? false,
|
||||
message_length: options?.messageLength,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackChatResponseReceived(searchSpaceId: number, chatId: number) {
|
||||
posthog.capture("chat_response_received", {
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackChatError(searchSpaceId: number, chatId: number, error?: string) {
|
||||
posthog.capture("chat_error", {
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DOCUMENT EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackDocumentUploadStarted(
|
||||
searchSpaceId: number,
|
||||
fileCount: number,
|
||||
totalSizeBytes: number
|
||||
) {
|
||||
posthog.capture("document_upload_started", {
|
||||
search_space_id: searchSpaceId,
|
||||
file_count: fileCount,
|
||||
total_size_bytes: totalSizeBytes,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackDocumentUploadSuccess(searchSpaceId: number, fileCount: number) {
|
||||
posthog.capture("document_upload_success", {
|
||||
search_space_id: searchSpaceId,
|
||||
file_count: fileCount,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackDocumentUploadFailure(searchSpaceId: number, error?: string) {
|
||||
posthog.capture("document_upload_failure", {
|
||||
search_space_id: searchSpaceId,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackDocumentDeleted(searchSpaceId: number, documentId: number) {
|
||||
posthog.capture("document_deleted", {
|
||||
search_space_id: searchSpaceId,
|
||||
document_id: documentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackDocumentBulkDeleted(searchSpaceId: number, count: number) {
|
||||
posthog.capture("document_bulk_deleted", {
|
||||
search_space_id: searchSpaceId,
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackYouTubeImport(searchSpaceId: number, url: string) {
|
||||
posthog.capture("youtube_import_started", {
|
||||
search_space_id: searchSpaceId,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONNECTOR EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackConnectorSetupStarted(searchSpaceId: number, connectorType: string) {
|
||||
posthog.capture("connector_setup_started", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackConnectorSetupSuccess(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number
|
||||
) {
|
||||
posthog.capture("connector_setup_success", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackConnectorSetupFailure(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
error?: string
|
||||
) {
|
||||
posthog.capture("connector_setup_failure", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackConnectorDeleted(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number
|
||||
) {
|
||||
posthog.capture("connector_deleted", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackConnectorSynced(
|
||||
searchSpaceId: number,
|
||||
connectorType: string,
|
||||
connectorId: number
|
||||
) {
|
||||
posthog.capture("connector_synced", {
|
||||
search_space_id: searchSpaceId,
|
||||
connector_type: connectorType,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SETTINGS EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackSettingsViewed(searchSpaceId: number, section: string) {
|
||||
posthog.capture("settings_viewed", {
|
||||
search_space_id: searchSpaceId,
|
||||
section,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSettingsUpdated(searchSpaceId: number, section: string, setting: string) {
|
||||
posthog.capture("settings_updated", {
|
||||
search_space_id: searchSpaceId,
|
||||
section,
|
||||
setting,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FEATURE USAGE EVENTS
|
||||
// ============================================
|
||||
|
||||
export function trackPodcastGenerated(searchSpaceId: number, chatId: number) {
|
||||
posthog.capture("podcast_generated", {
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackSourcesTabViewed(searchSpaceId: number, tab: string) {
|
||||
posthog.capture("sources_tab_viewed", {
|
||||
search_space_id: searchSpaceId,
|
||||
tab,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// USER IDENTIFICATION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Identify a user for PostHog analytics
|
||||
* Call this after successful authentication
|
||||
*/
|
||||
export function identifyUser(userId: string, properties?: Record<string, unknown>) {
|
||||
posthog.identify(userId, properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user identity (call on logout)
|
||||
*/
|
||||
export function resetUser() {
|
||||
posthog.reset();
|
||||
}
|
||||
17
surfsense_web/lib/posthog/server.ts
Normal file
17
surfsense_web/lib/posthog/server.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { PostHog } from "posthog-node";
|
||||
|
||||
export default function PostHogClient() {
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
throw new Error("NEXT_PUBLIC_POSTHOG_KEY is not set");
|
||||
}
|
||||
|
||||
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
// Because server-side functions in Next.js can be short-lived,
|
||||
// we set flushAt to 1 and flushInterval to 0 to ensure events are sent immediately
|
||||
flushAt: 1,
|
||||
flushInterval: 0,
|
||||
});
|
||||
|
||||
return posthogClient;
|
||||
}
|
||||
|
|
@ -1,20 +1,14 @@
|
|||
import type { GetChatsRequest } from "@/contracts/types/chat.types";
|
||||
import type { GetConnectorsRequest } from "@/contracts/types/connector.types";
|
||||
import type { GetDocumentsRequest } from "@/contracts/types/document.types";
|
||||
import type { GetLLMConfigsRequest } from "@/contracts/types/llm-config.types";
|
||||
import type { GetPodcastsRequest } from "@/contracts/types/podcast.types";
|
||||
import type { GetRolesRequest } from "@/contracts/types/roles.types";
|
||||
import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types";
|
||||
|
||||
export const cacheKeys = {
|
||||
chats: {
|
||||
activeChat: (chatId: string) => ["active-chat", chatId] as const,
|
||||
globalQueryParams: (queries: GetChatsRequest["queryParams"]) =>
|
||||
["chats", ...(queries ? Object.values(queries) : [])] as const,
|
||||
},
|
||||
podcasts: {
|
||||
globalQueryParams: (queries: GetPodcastsRequest["queryParams"]) =>
|
||||
["podcasts", ...(queries ? Object.values(queries) : [])] as const,
|
||||
// New chat threads (assistant-ui)
|
||||
threads: {
|
||||
list: (searchSpaceId: number) => ["threads", searchSpaceId] as const,
|
||||
detail: (threadId: number) => ["threads", "detail", threadId] as const,
|
||||
search: (searchSpaceId: number, query: string) =>
|
||||
["threads", "search", searchSpaceId, query] as const,
|
||||
},
|
||||
documents: {
|
||||
globalQueryParams: (queries: GetDocumentsRequest["queryParams"]) =>
|
||||
|
|
@ -25,13 +19,12 @@ export const cacheKeys = {
|
|||
typeCounts: (searchSpaceId?: string) => ["documents", "type-counts", searchSpaceId] as const,
|
||||
byChunk: (chunkId: string) => ["documents", "by-chunk", chunkId] as const,
|
||||
},
|
||||
llmConfigs: {
|
||||
global: () => ["llm-configs", "global"] as const,
|
||||
all: (searchSpaceId: string) => ["llm-configs", searchSpaceId] as const,
|
||||
withQueryParams: (queries: GetLLMConfigsRequest["queryParams"]) =>
|
||||
["llm-configs", ...(queries ? Object.values(queries) : [])] as const,
|
||||
byId: (llmConfigId: string) => ["llm-config", llmConfigId] as const,
|
||||
preferences: (searchSpaceId: string) => ["llm-preferences", searchSpaceId] as const,
|
||||
newLLMConfigs: {
|
||||
all: (searchSpaceId: number) => ["new-llm-configs", searchSpaceId] as const,
|
||||
byId: (configId: number) => ["new-llm-configs", "detail", configId] as const,
|
||||
preferences: (searchSpaceId: number) => ["llm-preferences", searchSpaceId] as const,
|
||||
defaultInstructions: () => ["new-llm-configs", "default-instructions"] as const,
|
||||
global: () => ["new-llm-configs", "global"] as const,
|
||||
},
|
||||
auth: {
|
||||
user: ["auth", "user"] as const,
|
||||
|
|
@ -41,7 +34,6 @@ export const cacheKeys = {
|
|||
withQueryParams: (queries: GetSearchSpacesRequest["queryParams"]) =>
|
||||
["search-spaces", ...(queries ? Object.values(queries) : [])] as const,
|
||||
detail: (searchSpaceId: string) => ["search-spaces", searchSpaceId] as const,
|
||||
communityPrompts: ["search-spaces", "community-prompts"] as const,
|
||||
},
|
||||
user: {
|
||||
current: () => ["user", "me"] as const,
|
||||
|
|
@ -53,6 +45,14 @@ export const cacheKeys = {
|
|||
permissions: {
|
||||
all: () => ["permissions"] as const,
|
||||
},
|
||||
members: {
|
||||
all: (searchSpaceId: string) => ["members", searchSpaceId] as const,
|
||||
myAccess: (searchSpaceId: string) => ["members", "my-access", searchSpaceId] as const,
|
||||
},
|
||||
invites: {
|
||||
all: (searchSpaceId: string) => ["invites", searchSpaceId] as const,
|
||||
info: (inviteCode: string) => ["invites", "info", inviteCode] as const,
|
||||
},
|
||||
connectors: {
|
||||
all: (searchSpaceId: string) => ["connectors", searchSpaceId] as const,
|
||||
withQueryParams: (queries: GetConnectorsRequest["queryParams"]) =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import { docs } from "@/.source";
|
||||
import { docs } from "@/.source/server";
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: "/docs",
|
||||
|
|
|
|||
|
|
@ -11,3 +11,11 @@ export function getChatTitleFromMessages(messages: Message[]) {
|
|||
if (userMessages.length === 0) return "Untitled Chat";
|
||||
return userMessages[0].content;
|
||||
}
|
||||
|
||||
export const formatDate = (date: Date): string => {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue