feat: added shared chats

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-01-13 00:17:12 -08:00
parent 764dd05582
commit f22d649239
22 changed files with 1881 additions and 506 deletions

View file

@ -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} />

View file

@ -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"
>

View file

@ -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>