mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/inbox
This commit is contained in:
commit
614761bb17
64 changed files with 2604 additions and 730 deletions
|
|
@ -23,7 +23,10 @@ export type RequestOptions = {
|
|||
class BaseApiService {
|
||||
baseUrl: string;
|
||||
|
||||
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"]; // Add more endpoints as needed
|
||||
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"];
|
||||
|
||||
// Prefixes that don't require auth (checked with startsWith)
|
||||
noAuthPrefixes: string[] = ["/api/v1/public/", "/api/v1/podcasts/"];
|
||||
|
||||
// Use a getter to always read fresh token from localStorage
|
||||
// This ensures the token is always up-to-date after login/logout
|
||||
|
|
@ -84,7 +87,10 @@ class BaseApiService {
|
|||
}
|
||||
|
||||
// Validate the bearer token
|
||||
if (!this.bearerToken && !this.noAuthEndpoints.includes(url)) {
|
||||
const isNoAuthEndpoint =
|
||||
this.noAuthEndpoints.includes(url) ||
|
||||
this.noAuthPrefixes.some((prefix) => url.startsWith(prefix));
|
||||
if (!this.bearerToken && !isNoAuthEndpoint) {
|
||||
throw new AuthenticationError("You are not authenticated. Please login again.");
|
||||
}
|
||||
|
||||
|
|
|
|||
33
surfsense_web/lib/apis/chat-threads-api.service.ts
Normal file
33
surfsense_web/lib/apis/chat-threads-api.service.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
type TogglePublicShareRequest,
|
||||
type TogglePublicShareResponse,
|
||||
togglePublicShareRequest,
|
||||
togglePublicShareResponse,
|
||||
} from "@/contracts/types/chat-threads.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class ChatThreadsApiService {
|
||||
/**
|
||||
* Toggle public sharing for a thread.
|
||||
* Requires authentication.
|
||||
*/
|
||||
togglePublicShare = async (
|
||||
request: TogglePublicShareRequest
|
||||
): Promise<TogglePublicShareResponse> => {
|
||||
const parsed = togglePublicShareRequest.safeParse(request);
|
||||
|
||||
if (!parsed.success) {
|
||||
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.patch(
|
||||
`/api/v1/threads/${parsed.data.thread_id}/public-share`,
|
||||
togglePublicShareResponse,
|
||||
{ body: { enabled: parsed.data.enabled } }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const chatThreadsApiService = new ChatThreadsApiService();
|
||||
73
surfsense_web/lib/apis/public-chat-api.service.ts
Normal file
73
surfsense_web/lib/apis/public-chat-api.service.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
type ClonePublicChatRequest,
|
||||
type ClonePublicChatResponse,
|
||||
type CompleteCloneRequest,
|
||||
type CompleteCloneResponse,
|
||||
clonePublicChatRequest,
|
||||
clonePublicChatResponse,
|
||||
completeCloneRequest,
|
||||
completeCloneResponse,
|
||||
type GetPublicChatRequest,
|
||||
type GetPublicChatResponse,
|
||||
getPublicChatRequest,
|
||||
getPublicChatResponse,
|
||||
} from "@/contracts/types/public-chat.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
class PublicChatApiService {
|
||||
/**
|
||||
* Get a public chat by share token.
|
||||
* No authentication required.
|
||||
*/
|
||||
getPublicChat = async (request: GetPublicChatRequest): Promise<GetPublicChatResponse> => {
|
||||
const parsed = getPublicChatRequest.safeParse(request);
|
||||
|
||||
if (!parsed.success) {
|
||||
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.get(`/api/v1/public/${parsed.data.share_token}`, getPublicChatResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a public chat to the user's account.
|
||||
* Creates an empty thread and returns thread_id for redirect.
|
||||
* Requires authentication.
|
||||
*/
|
||||
clonePublicChat = async (request: ClonePublicChatRequest): Promise<ClonePublicChatResponse> => {
|
||||
const parsed = clonePublicChatRequest.safeParse(request);
|
||||
|
||||
if (!parsed.success) {
|
||||
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(
|
||||
`/api/v1/public/${parsed.data.share_token}/clone`,
|
||||
clonePublicChatResponse
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete the clone by copying messages and podcasts.
|
||||
* Called from the chat page after redirect.
|
||||
* Requires authentication.
|
||||
*/
|
||||
completeClone = async (request: CompleteCloneRequest): Promise<CompleteCloneResponse> => {
|
||||
const parsed = completeCloneRequest.safeParse(request);
|
||||
|
||||
if (!parsed.success) {
|
||||
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
|
||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return baseApiService.post(
|
||||
`/api/v1/threads/${parsed.data.thread_id}/complete-clone`,
|
||||
completeCloneResponse
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const publicChatApiService = new PublicChatApiService();
|
||||
109
surfsense_web/lib/chat/message-utils.ts
Normal file
109
surfsense_web/lib/chat/message-utils.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||
import { z } from "zod";
|
||||
import type { MessageRecord } from "./thread-persistence";
|
||||
|
||||
/**
|
||||
* Zod schema for persisted attachment info
|
||||
*/
|
||||
const PersistedAttachmentSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
contentType: z.string().optional(),
|
||||
imageDataUrl: z.string().optional(),
|
||||
extractedContent: z.string().optional(),
|
||||
});
|
||||
|
||||
const AttachmentsPartSchema = z.object({
|
||||
type: z.literal("attachments"),
|
||||
items: z.array(PersistedAttachmentSchema),
|
||||
});
|
||||
|
||||
type PersistedAttachment = z.infer<typeof PersistedAttachmentSchema>;
|
||||
|
||||
/**
|
||||
* Extract persisted attachments from message content (type-safe with Zod)
|
||||
*/
|
||||
function extractPersistedAttachments(content: unknown): PersistedAttachment[] {
|
||||
if (!Array.isArray(content)) return [];
|
||||
|
||||
for (const part of content) {
|
||||
const result = AttachmentsPartSchema.safeParse(part);
|
||||
if (result.success) {
|
||||
return result.data.items;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert backend message to assistant-ui ThreadMessageLike format
|
||||
* Filters out 'thinking-steps' part as it's handled separately via messageThinkingSteps
|
||||
* Restores attachments for user messages from persisted data
|
||||
*/
|
||||
export function convertToThreadMessage(msg: MessageRecord): ThreadMessageLike {
|
||||
let content: ThreadMessageLike["content"];
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
content = [{ type: "text", text: msg.content }];
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Filter out custom metadata parts - they're handled separately
|
||||
const filteredContent = msg.content.filter((part: unknown) => {
|
||||
if (typeof part !== "object" || part === null || !("type" in part)) return true;
|
||||
const partType = (part as { type: string }).type;
|
||||
// Filter out thinking-steps, mentioned-documents, and attachments
|
||||
return (
|
||||
partType !== "thinking-steps" &&
|
||||
partType !== "mentioned-documents" &&
|
||||
partType !== "attachments"
|
||||
);
|
||||
});
|
||||
content =
|
||||
filteredContent.length > 0
|
||||
? (filteredContent as ThreadMessageLike["content"])
|
||||
: [{ type: "text", text: "" }];
|
||||
} else {
|
||||
content = [{ type: "text", text: String(msg.content) }];
|
||||
}
|
||||
|
||||
// Restore attachments for user messages
|
||||
let attachments: ThreadMessageLike["attachments"];
|
||||
if (msg.role === "user") {
|
||||
const persistedAttachments = extractPersistedAttachments(msg.content);
|
||||
if (persistedAttachments.length > 0) {
|
||||
attachments = persistedAttachments.map((att) => ({
|
||||
id: att.id,
|
||||
name: att.name,
|
||||
type: att.type as "document" | "image" | "file",
|
||||
contentType: att.contentType || "application/octet-stream",
|
||||
status: { type: "complete" as const },
|
||||
content: [],
|
||||
// Custom fields for our ChatAttachment interface
|
||||
imageDataUrl: att.imageDataUrl,
|
||||
extractedContent: att.extractedContent,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata.custom for author display in shared chats
|
||||
const metadata = msg.author_id
|
||||
? {
|
||||
custom: {
|
||||
author: {
|
||||
displayName: msg.author_display_name ?? null,
|
||||
avatarUrl: msg.author_avatar_url ?? null,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: `msg-${msg.id}`,
|
||||
role: msg.role,
|
||||
content,
|
||||
createdAt: new Date(msg.created_at),
|
||||
attachments,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@ export interface ThreadRecord {
|
|||
created_at: string;
|
||||
updated_at: string;
|
||||
has_comments?: boolean;
|
||||
public_share_enabled?: boolean;
|
||||
public_share_token?: string | null;
|
||||
clone_pending?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageRecord {
|
||||
|
|
|
|||
|
|
@ -79,4 +79,7 @@ export const cacheKeys = {
|
|||
comments: {
|
||||
byMessage: (messageId: number) => ["comments", "message", messageId] as const,
|
||||
},
|
||||
publicChat: {
|
||||
byToken: (shareToken: string) => ["public-chat", shareToken] as const,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue