diff --git a/surfsense_backend/app/schemas/new_chat.py b/surfsense_backend/app/schemas/new_chat.py index 61af0d92c..84ba1b950 100644 --- a/surfsense_backend/app/schemas/new_chat.py +++ b/surfsense_backend/app/schemas/new_chat.py @@ -246,6 +246,7 @@ class PublicChatSnapshotDetail(BaseModel): message_count: int thread_id: int thread_title: str + created_by_user_id: str | None = None class PublicChatSnapshotsBySpaceResponse(BaseModel): diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py index 4da316240..9088ed748 100644 --- a/surfsense_backend/app/services/public_chat_service.py +++ b/surfsense_backend/app/services/public_chat_service.py @@ -439,6 +439,7 @@ async def list_snapshots_for_search_space( "message_count": len(s.message_ids) if s.message_ids else 0, "thread_id": s.thread_id, "thread_title": thread_titles.get(s.thread_id, "Untitled"), + "created_by_user_id": str(s.created_by_user_id) if s.created_by_user_id else None, } for s in snapshots ] diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx index 5f0048100..d27166bf1 100644 --- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx +++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshot-row.tsx @@ -1,15 +1,28 @@ "use client"; -import { Copy, MessageSquare, Trash2 } from "lucide-react"; +import { Copy, ExternalLink, MessageSquare, Trash2 } from "lucide-react"; +import Image from "next/image"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types"; +function getInitials(name: string): string { + const parts = name.trim().split(/\s+/); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +} + interface PublicChatSnapshotRowProps { snapshot: PublicChatSnapshotDetail; canDelete: boolean; onCopy: (snapshot: PublicChatSnapshotDetail) => void; onDelete: (snapshot: PublicChatSnapshotDetail) => void; isDeleting?: boolean; + memberMap: Map; } export function PublicChatSnapshotRow({ @@ -18,6 +31,7 @@ export function PublicChatSnapshotRow({ onCopy, onDelete, isDeleting = false, + memberMap, }: PublicChatSnapshotRowProps) { const formattedDate = new Date(snapshot.created_at).toLocaleDateString(undefined, { year: "numeric", @@ -25,50 +39,127 @@ export function PublicChatSnapshotRow({ day: "numeric", }); + const member = snapshot.created_by_user_id + ? memberMap.get(snapshot.created_by_user_id) + : null; + return ( -
-
-

- {snapshot.thread_title} -

-
- {formattedDate} - - - {snapshot.message_count} - + + + {/* Header: Title + Actions */} +
+
+

+ {snapshot.thread_title} +

+
+
+ + + + + + Copy link + + + + + + + + + + Open link + + + {canDelete && ( + + + + + + Delete + + + )} +
- (e.target as HTMLInputElement).select()} - /> -
-
- - {canDelete && ( - - )} -
-
+ + {snapshot.message_count} messages + +
+ + {/* Footer: Date + Creator */} +
+ + {formattedDate} + + {member && ( + <> + · + + + +
+ {member.avatarUrl ? ( + {member.name} + ) : ( +
+ + {getInitials(member.name)} + +
+ )} + + {member.name} + +
+
+ + {member.email || member.name} + +
+
+ + )} +
+ + ); } diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx index 38c435059..8daf9a07f 100644 --- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx +++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-list.tsx @@ -10,6 +10,7 @@ interface PublicChatSnapshotsListProps { onCopy: (snapshot: PublicChatSnapshotDetail) => void; onDelete: (snapshot: PublicChatSnapshotDetail) => void; deletingId?: number; + memberMap: Map; } export function PublicChatSnapshotsList({ @@ -18,13 +19,14 @@ export function PublicChatSnapshotsList({ onCopy, onDelete, deletingId, + memberMap, }: PublicChatSnapshotsListProps) { if (snapshots.length === 0) { return ; } return ( -
+
{snapshots.map((snapshot) => ( ))}
diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx index 167443f66..07ff57c0d 100644 --- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx +++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx @@ -1,14 +1,13 @@ "use client"; import { useAtomValue } from "jotai"; -import { AlertCircle, Globe, Info } from "lucide-react"; +import { AlertCircle, Info } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; -import { myAccessAtom } from "@/atoms/members/members-query.atoms"; +import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { deletePublicChatSnapshotMutationAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-mutation.atoms"; import { publicChatSnapshotsAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-query.atoms"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types"; import { PublicChatSnapshotsList } from "./public-chat-snapshots-list"; @@ -25,6 +24,22 @@ export function PublicChatSnapshotsManager({ // Data fetching const { data: snapshotsData, isLoading, isError } = useAtomValue(publicChatSnapshotsAtom); + // Members for user resolution + const { data: members } = useAtomValue(membersAtom); + const memberMap = useMemo(() => { + const map = new Map(); + if (members) { + for (const m of members) { + map.set(m.user_id, { + name: m.user_display_name || m.user_email || "Unknown", + email: m.user_email || undefined, + avatarUrl: m.user_avatar_url || undefined, + }); + } + } + return map; + }, [members]); + // Permissions const { data: access } = useAtomValue(myAccessAtom); const canView = useMemo(() => { @@ -69,16 +84,13 @@ export function PublicChatSnapshotsManager({ // Loading state if (isLoading) { return ( -
- - - - - - - - - +
+ +
+ + + +
); } @@ -110,35 +122,23 @@ export function PublicChatSnapshotsManager({ const snapshots = snapshotsData?.snapshots ?? []; return ( -
- - +
+ + Public chat links allow anyone with the URL to view a snapshot of a chat. These links do not update when the original chat changes. - - - - - Public Chat Links - - - Manage public links to chats in this search space. - - - - - - +
); } diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx index e87cc9a95..013b7cae2 100644 --- a/surfsense_web/components/settings/image-model-manager.tsx +++ b/surfsense_web/components/settings/image-model-manager.tsx @@ -12,13 +12,15 @@ import { Plus, RefreshCw, Shuffle, - Sparkles, + Info, Trash2, + User, Wand2, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { membersAtom } from "@/atoms/members/members-query.atoms"; import { createImageGenConfigMutationAtom, deleteImageGenConfigMutationAtom, @@ -122,6 +124,21 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { useAtomValue(globalImageGenConfigsAtom); const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom); + // Members for user resolution + const { data: members } = useAtomValue(membersAtom); + const memberMap = useMemo(() => { + const map = new Map(); + if (members) { + for (const m of members) { + map.set(m.user_id, { + name: m.user_display_name || m.user_email || "Unknown", + email: m.user_email || undefined, + }); + } + } + return map; + }, [members]); + // Local state const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingConfig, setEditingConfig] = useState(null); @@ -320,16 +337,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {/* Global info */} {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && ( - - - - - {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global - image model(s) - {" "} - available from your administrator. - - + + + + + {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global + image model(s) + {" "} + available from your administrator. + + )} {/* Active Preference Card */} @@ -521,9 +538,17 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {config.description}

)} -
- - {new Date(config.created_at).toLocaleDateString()} +
+
+ + {new Date(config.created_at).toLocaleDateString()} +
+ {config.user_id && memberMap.get(config.user_id) && ( +
+ + {memberMap.get(config.user_id)?.name} +
+ )}
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx index dac68a358..d91150a4f 100644 --- a/surfsense_web/components/settings/llm-role-manager.tsx +++ b/surfsense_web/components/settings/llm-role-manager.tsx @@ -239,7 +239,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { ) : !isAssignmentComplete ? ( - + Complete all role assignments to enable full functionality. Each role serves @@ -247,7 +247,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { ) : ( - + All roles are assigned and ready to use! Your LLM configuration is complete. diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index bdd951349..3ee5458b8 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -3,19 +3,19 @@ import { useAtomValue } from "jotai"; import { AlertCircle, - Bot, - Clock, Edit3, FileText, MessageSquareQuote, Plus, RefreshCw, - Sparkles, + Info, Trash2, Wand2, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useState } from "react"; +import Image from "next/image"; +import { useCallback, useMemo, useState } from "react"; +import { membersAtom } from "@/atoms/members/members-query.atoms"; import { createNewLLMConfigMutationAtom, deleteNewLLMConfigMutationAtom, @@ -71,6 +71,14 @@ const item = { show: { opacity: 1, y: 0 }, }; +function getInitials(name: string): string { + const parts = name.trim().split(/\s+/); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +} + export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { // Mutations const { @@ -98,6 +106,22 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { } = useAtomValue(newLLMConfigsAtom); const { data: globalConfigs = [] } = useAtomValue(globalNewLLMConfigsAtom); + // Members for user resolution + const { data: members } = useAtomValue(membersAtom); + const memberMap = useMemo(() => { + const map = new Map(); + if (members) { + for (const m of members) { + map.set(m.user_id, { + name: m.user_display_name || m.user_email || "Unknown", + email: m.user_email || undefined, + avatarUrl: m.user_avatar_url || undefined, + }); + } + } + return map; + }, [members]); + // Local state const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingConfig, setEditingConfig] = useState(null); @@ -153,21 +177,27 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { }; return ( -
- {/* Header */} -
-
- -
+
+ {/* Header actions */} +
+ +
{/* Error Alerts */} @@ -193,12 +223,12 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { {/* Global Configs Info */} {globalConfigs.length > 0 && ( - - - + + + {globalConfigs.length} global configuration(s){" "} available from your administrator. These are pre-configured and ready to use.{" "} - + Global configs: {globalConfigs.map((g) => g.name).join(", ")} @@ -222,18 +252,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { {/* Configurations List */} {!isLoading && ( -
-
-

Your Configurations

- -
- +
{configs?.length === 0 ? ( @@ -259,9 +278,16 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { ) : ( - + {configs?.map((config) => { + const member = config.user_id ? memberMap.get(config.user_id) : null; + return ( - - -
- {/* Left accent bar */} -
+ + + {/* Header: Name + Actions */} +
+
+

+ {config.name} +

+ {config.description && ( +

+ {config.description} +

+ )} +
+
+ + + + + + Edit + + + + + + + + Delete + + +
+
-
-
- {/* Main content */} -
-
- -
-
- {/* Title row */} -
-

- {config.name} -

-
- - {config.provider} - - {config.citations_enabled && ( - - - - - - Citations - - - - Citations are enabled for this configuration - - - + {/* Provider + Model */} +
+ + {config.provider} + + + {config.model_name} + +
+ + {/* Feature badges */} +
+ {config.citations_enabled && ( + + + Citations + + )} + {!config.use_default_system_instructions && + config.system_instructions && ( + + + Custom + + )} +
+ + {/* Footer: Date + Creator */} +
+ + {new Date(config.created_at).toLocaleDateString( + undefined, + { + year: "numeric", + month: "short", + day: "numeric", + } + )} + + {member && ( + <> + · + + + +
+ {member.avatarUrl ? ( + {member.name} + ) : ( +
+ + {getInitials(member.name)} + +
)} - {!config.use_default_system_instructions && - config.system_instructions && ( - - - - - - Custom - - - - Using custom system instructions - - - - )} -
-
- - {/* Model name */} - - {config.model_name} - - - {/* Description if any */} - {config.description && ( -

- {config.description} -

- )} - - {/* Footer row */} -
-
- - - {new Date(config.created_at).toLocaleDateString()} + + {member.name}
-
-
-
- - {/* Actions */} -
- - - - - - Edit - - - - - - - - Delete - - -
-
-
+ + + {member.email || member.name} + + + + + )}
@@ -408,7 +439,10 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { {/* Add/Edit Configuration Dialog */} !open && closeDialog()}> - + e.preventDefault()} + > {editingConfig ? ( diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx index 64f6adf23..54058759f 100644 --- a/surfsense_web/components/settings/prompt-config-manager.tsx +++ b/surfsense_web/components/settings/prompt-config-manager.tsx @@ -122,7 +122,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) - + System instructions apply to all AI interactions in this search space. They guide how the diff --git a/surfsense_web/contracts/types/chat-threads.types.ts b/surfsense_web/contracts/types/chat-threads.types.ts index df561092e..d245a4168 100644 --- a/surfsense_web/contracts/types/chat-threads.types.ts +++ b/surfsense_web/contracts/types/chat-threads.types.ts @@ -55,6 +55,7 @@ export const publicChatSnapshotDetail = z.object({ message_count: z.number(), thread_id: z.number(), thread_title: z.string(), + created_by_user_id: z.string().nullable().optional(), }); /**