mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-15 18:25:18 +02:00
feat: added shared chats
This commit is contained in:
parent
764dd05582
commit
f22d649239
22 changed files with 1881 additions and 506 deletions
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { RefreshCw, SquarePlus } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -34,8 +34,13 @@ export default function DocumentsTable() {
|
|||
const t = useTranslations("documents");
|
||||
const id = useId();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const searchSpaceId = Number(params.search_space_id);
|
||||
|
||||
const handleNewNote = useCallback(() => {
|
||||
router.push(`/dashboard/${searchSpaceId}/editor/new`);
|
||||
}, [router, searchSpaceId]);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounced(search, 250);
|
||||
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
||||
|
|
@ -238,10 +243,16 @@ export default function DocumentsTable() {
|
|||
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||
</div>
|
||||
<Button onClick={refreshCurrentView} variant="outline" size="sm" disabled={isRefreshing}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handleNewNote} variant="default" size="sm">
|
||||
<SquarePlus className="w-4 h-4 mr-2" />
|
||||
{t("create_shared_note")}
|
||||
</Button>
|
||||
<Button onClick={refreshCurrentView} variant="outline" size="sm" disabled={isRefreshing}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<ProcessingIndicator documentProcessorTasksCount={documentProcessorTasksCount} />
|
||||
|
|
|
|||
|
|
@ -267,21 +267,8 @@ export default function EditorPage() {
|
|||
setHasUnsavedChanges(false);
|
||||
toast.success("Note created successfully! Reindexing in background...");
|
||||
|
||||
// Invalidate notes query to refresh the sidebar
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["notes", String(searchSpaceId)],
|
||||
});
|
||||
|
||||
// Update URL to reflect the new document ID without navigation
|
||||
window.history.replaceState({}, "", `/dashboard/${searchSpaceId}/editor/${note.id}`);
|
||||
// Update document state to reflect the new ID
|
||||
setDocument({
|
||||
document_id: note.id,
|
||||
title: title,
|
||||
document_type: "NOTE",
|
||||
blocknote_document: editorContent,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
// Redirect to documents page after successful save
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
} else {
|
||||
// Existing document - save normally
|
||||
if (!editorContent) {
|
||||
|
|
@ -310,12 +297,8 @@ export default function EditorPage() {
|
|||
setHasUnsavedChanges(false);
|
||||
toast.success("Document saved! Reindexing in background...");
|
||||
|
||||
// Invalidate notes query when updating notes to refresh the sidebar
|
||||
if (isNote) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["notes", String(searchSpaceId)],
|
||||
});
|
||||
}
|
||||
// Redirect to documents page after successful save
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving document:", error);
|
||||
|
|
@ -336,7 +319,7 @@ export default function EditorPage() {
|
|||
if (hasUnsavedChanges) {
|
||||
setShowUnsavedDialog(true);
|
||||
} else {
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -346,12 +329,12 @@ export default function EditorPage() {
|
|||
setGlobalHasUnsavedChanges(false);
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// If there's a pending navigation (from sidebar), use that; otherwise go back to chat
|
||||
// If there's a pending navigation (from sidebar), use that; otherwise go back to documents
|
||||
if (pendingNavigation) {
|
||||
router.push(pendingNavigation);
|
||||
setPendingNavigation(null);
|
||||
} else {
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
||||
router.push(`/dashboard/${searchSpaceId}/documents`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -392,7 +375,7 @@ export default function EditorPage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/new-chat`)}
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/documents`)}
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@ import {
|
|||
} from "@/lib/chat/podcast-state";
|
||||
import {
|
||||
appendMessage,
|
||||
type ChatVisibility,
|
||||
createThread,
|
||||
getThreadFull,
|
||||
getThreadMessages,
|
||||
type MessageRecord,
|
||||
type ThreadRecord,
|
||||
} from "@/lib/chat/thread-persistence";
|
||||
import {
|
||||
trackChatCreated,
|
||||
|
|
@ -217,6 +220,7 @@ export default function NewChatPage() {
|
|||
const queryClient = useQueryClient();
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [threadId, setThreadId] = useState<number | null>(null);
|
||||
const [currentThread, setCurrentThread] = useState<ThreadRecord | null>(null);
|
||||
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
// Store thinking steps per message ID - kept separate from content to avoid
|
||||
|
|
@ -264,6 +268,7 @@ export default function NewChatPage() {
|
|||
// Reset all state when switching between chats to prevent stale data
|
||||
setMessages([]);
|
||||
setThreadId(null);
|
||||
setCurrentThread(null);
|
||||
setMessageThinkingSteps(new Map());
|
||||
setMentionedDocumentIds([]);
|
||||
setMentionedDocuments([]);
|
||||
|
|
@ -272,11 +277,19 @@ export default function NewChatPage() {
|
|||
|
||||
try {
|
||||
if (urlChatId > 0) {
|
||||
// Thread exists - load messages
|
||||
// Thread exists - load thread data and messages
|
||||
setThreadId(urlChatId);
|
||||
const response = await getThreadMessages(urlChatId);
|
||||
if (response.messages && response.messages.length > 0) {
|
||||
const loadedMessages = response.messages.map(convertToThreadMessage);
|
||||
|
||||
// Load thread data (for visibility info) and messages in parallel
|
||||
const [threadData, messagesResponse] = await Promise.all([
|
||||
getThreadFull(urlChatId),
|
||||
getThreadMessages(urlChatId),
|
||||
]);
|
||||
|
||||
setCurrentThread(threadData);
|
||||
|
||||
if (messagesResponse.messages && messagesResponse.messages.length > 0) {
|
||||
const loadedMessages = messagesResponse.messages.map(convertToThreadMessage);
|
||||
setMessages(loadedMessages);
|
||||
|
||||
// Extract and restore thinking steps from persisted messages
|
||||
|
|
@ -284,7 +297,7 @@ export default function NewChatPage() {
|
|||
// Extract and restore mentioned documents from persisted messages
|
||||
const restoredDocsMap: Record<string, MentionedDocumentInfo[]> = {};
|
||||
|
||||
for (const msg of response.messages) {
|
||||
for (const msg of messagesResponse.messages) {
|
||||
if (msg.role === "assistant") {
|
||||
const steps = extractThinkingSteps(msg.content);
|
||||
if (steps.length > 0) {
|
||||
|
|
@ -320,6 +333,7 @@ export default function NewChatPage() {
|
|||
// Keep threadId as null - don't use Date.now() as it creates an invalid ID
|
||||
// that will cause 404 errors on subsequent API calls
|
||||
setThreadId(null);
|
||||
setCurrentThread(null);
|
||||
toast.error("Failed to load chat. Please try again.");
|
||||
} finally {
|
||||
setIsInitializing(false);
|
||||
|
|
@ -346,6 +360,19 @@ export default function NewChatPage() {
|
|||
setIsRunning(false);
|
||||
}, []);
|
||||
|
||||
// Handle visibility change from ChatShareButton
|
||||
const handleVisibilityChange = useCallback(
|
||||
(newVisibility: ChatVisibility) => {
|
||||
setCurrentThread((prev) => (prev ? { ...prev, visibility: newVisibility } : null));
|
||||
// Refetch all thread queries so sidebar reflects the change immediately
|
||||
// Use predicate to match any query that starts with "threads"
|
||||
queryClient.refetchQueries({
|
||||
predicate: (query) => Array.isArray(query.queryKey) && query.queryKey[0] === "threads",
|
||||
});
|
||||
},
|
||||
[queryClient]
|
||||
);
|
||||
|
||||
// Handle new message from user
|
||||
const onNew = useCallback(
|
||||
async (message: AppendMessage) => {
|
||||
|
|
@ -916,7 +943,13 @@ export default function NewChatPage() {
|
|||
<div className="flex flex-col h-[calc(100vh-64px)] overflow-hidden">
|
||||
<Thread
|
||||
messageThinkingSteps={messageThinkingSteps}
|
||||
header={<ChatHeader searchSpaceId={searchSpaceId} />}
|
||||
header={
|
||||
<ChatHeader
|
||||
searchSpaceId={searchSpaceId}
|
||||
thread={currentThread}
|
||||
onThreadVisibilityChange={handleVisibilityChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue