mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
fix: formatting
This commit is contained in:
parent
96bf469a1f
commit
deec8c5c6c
5 changed files with 183 additions and 127 deletions
|
|
@ -181,7 +181,9 @@ async def stream_new_chat(
|
|||
context_parts.append(format_attachments_as_context(attachments))
|
||||
|
||||
if mentioned_documents:
|
||||
context_parts.append(format_mentioned_documents_as_context(mentioned_documents))
|
||||
context_parts.append(
|
||||
format_mentioned_documents_as_context(mentioned_documents)
|
||||
)
|
||||
|
||||
if context_parts:
|
||||
context = "\n\n".join(context_parts)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ import { useAtomValue, useSetAtom } from "jotai";
|
|||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, messageDocumentsMapAtom, type MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import {
|
||||
type MentionedDocumentInfo,
|
||||
mentionedDocumentIdsAtom,
|
||||
mentionedDocumentsAtom,
|
||||
messageDocumentsMapAtom,
|
||||
} from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { ChatHeader } from "@/components/new-chat/chat-header";
|
||||
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||
|
|
@ -54,15 +59,15 @@ function extractThinkingSteps(content: unknown): ThinkingStep[] {
|
|||
*/
|
||||
function extractMentionedDocuments(content: unknown): MentionedDocumentInfo[] {
|
||||
if (!Array.isArray(content)) return [];
|
||||
|
||||
|
||||
const docsPart = content.find(
|
||||
(part: unknown) =>
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
(part: unknown) =>
|
||||
typeof part === "object" &&
|
||||
part !== null &&
|
||||
"type" in part &&
|
||||
(part as { type: string }).type === "mentioned-documents"
|
||||
) as { type: "mentioned-documents"; documents: MentionedDocumentInfo[] } | undefined;
|
||||
|
||||
|
||||
return docsPart?.documents || [];
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +184,7 @@ export default function NewChatPage() {
|
|||
const restoredThinkingSteps = new Map<string, ThinkingStep[]>();
|
||||
// Extract and restore mentioned documents from persisted messages
|
||||
const restoredDocsMap: Record<string, MentionedDocumentInfo[]> = {};
|
||||
|
||||
|
||||
for (const msg of response.messages) {
|
||||
if (msg.role === "assistant") {
|
||||
const steps = extractThinkingSteps(msg.content);
|
||||
|
|
@ -292,16 +297,20 @@ export default function NewChatPage() {
|
|||
}
|
||||
|
||||
// Persist user message with mentioned documents (don't await, fire and forget)
|
||||
const persistContent = mentionedDocuments.length > 0
|
||||
? [
|
||||
...message.content,
|
||||
{ type: "mentioned-documents", documents: mentionedDocuments.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
document_type: doc.document_type,
|
||||
})) },
|
||||
]
|
||||
: message.content;
|
||||
const persistContent =
|
||||
mentionedDocuments.length > 0
|
||||
? [
|
||||
...message.content,
|
||||
{
|
||||
type: "mentioned-documents",
|
||||
documents: mentionedDocuments.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
document_type: doc.document_type,
|
||||
})),
|
||||
},
|
||||
]
|
||||
: message.content;
|
||||
appendMessage(threadId, {
|
||||
role: "user",
|
||||
content: persistContent,
|
||||
|
|
@ -626,7 +635,16 @@ export default function NewChatPage() {
|
|||
// Note: We no longer clear thinking steps - they persist with the message
|
||||
}
|
||||
},
|
||||
[threadId, searchSpaceId, messages, mentionedDocumentIds, mentionedDocuments, setMentionedDocumentIds, setMentionedDocuments, setMessageDocumentsMap]
|
||||
[
|
||||
threadId,
|
||||
searchSpaceId,
|
||||
messages,
|
||||
mentionedDocumentIds,
|
||||
mentionedDocuments,
|
||||
setMentionedDocumentIds,
|
||||
setMentionedDocuments,
|
||||
setMessageDocumentsMap,
|
||||
]
|
||||
);
|
||||
|
||||
// Convert message (pass through since already in correct format)
|
||||
|
|
|
|||
|
|
@ -29,4 +29,3 @@ export interface MentionedDocumentInfo {
|
|||
* This allows displaying which documents were mentioned with each user message.
|
||||
*/
|
||||
export const messageDocumentsMapAtom = atom<Record<string, MentionedDocumentInfo[]>>({});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import {
|
|||
MessagePrimitive,
|
||||
ThreadPrimitive,
|
||||
useAssistantState,
|
||||
useThreadViewport,
|
||||
useMessage,
|
||||
useThreadViewport,
|
||||
} from "@assistant-ui/react";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import {
|
||||
|
|
@ -35,9 +35,23 @@ import {
|
|||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { type FC, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
type FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, messageDocumentsMapAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
||||
import {
|
||||
mentionedDocumentIdsAtom,
|
||||
mentionedDocumentsAtom,
|
||||
messageDocumentsMapAtom,
|
||||
} from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
||||
import {
|
||||
globalNewLLMConfigsAtom,
|
||||
|
|
@ -54,7 +68,10 @@ import {
|
|||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { DocumentsDataTable, type DocumentsDataTableRef } from "@/components/new-chat/DocumentsDataTable";
|
||||
import {
|
||||
DocumentsDataTable,
|
||||
type DocumentsDataTableRef,
|
||||
} from "@/components/new-chat/DocumentsDataTable";
|
||||
import {
|
||||
ChainOfThought,
|
||||
ChainOfThoughtContent,
|
||||
|
|
@ -67,7 +84,6 @@ import { Button } from "@/components/ui/button";
|
|||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { Document } from "@/contracts/types/document.types";
|
||||
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
||||
import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -371,7 +387,7 @@ const getTimeBasedGreeting = (userEmail?: string): string => {
|
|||
|
||||
const ThreadWelcome: FC = () => {
|
||||
const { data: user } = useAtomValue(currentUserAtom);
|
||||
|
||||
|
||||
// Memoize greeting so it doesn't change on re-renders (only on user change)
|
||||
const greeting = useMemo(() => getTimeBasedGreeting(user?.email), [user?.email]);
|
||||
|
||||
|
|
@ -427,14 +443,14 @@ const Composer: FC = () => {
|
|||
// Check if value contains @ and extract query
|
||||
if (value.includes("@")) {
|
||||
const query = extractMentionQuery(value);
|
||||
|
||||
|
||||
// Close popup if query starts with space (user typed "@ ")
|
||||
if (query.startsWith(" ")) {
|
||||
setShowDocumentPopover(false);
|
||||
setMentionQuery("");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Reopen popup if @ is present and query doesn't start with space
|
||||
// (handles case where user deleted the space after @)
|
||||
if (!showDocumentPopover) {
|
||||
|
|
@ -504,7 +520,7 @@ const Composer: FC = () => {
|
|||
const input = inputRef.current;
|
||||
const currentValue = input.value;
|
||||
const atIndex = currentValue.lastIndexOf("@");
|
||||
|
||||
|
||||
if (atIndex !== -1) {
|
||||
// Remove @ and everything after it
|
||||
const newValue = currentValue.slice(0, atIndex);
|
||||
|
|
@ -520,7 +536,7 @@ const Composer: FC = () => {
|
|||
// Focus the input so user can continue typing
|
||||
input.focus();
|
||||
}
|
||||
|
||||
|
||||
// Reset mention query
|
||||
setMentionQuery("");
|
||||
};
|
||||
|
|
@ -558,7 +574,11 @@ const Composer: FC = () => {
|
|||
ref={inputRef}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={mentionedDocuments.length > 0 ? "Ask about these documents..." : "Ask SurfSense (type @ to mention docs)"}
|
||||
placeholder={
|
||||
mentionedDocuments.length > 0
|
||||
? "Ask about these documents..."
|
||||
: "Ask SurfSense (type @ to mention docs)"
|
||||
}
|
||||
className="aui-composer-input flex-1 min-w-[120px] max-h-32 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0 py-1"
|
||||
rows={1}
|
||||
autoFocus
|
||||
|
|
@ -567,41 +587,47 @@ const Composer: FC = () => {
|
|||
</div>
|
||||
|
||||
{/* -------- Document mention popover (rendered via portal) -------- */}
|
||||
{showDocumentPopover && typeof document !== "undefined" && createPortal(
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 cursor-default"
|
||||
style={{ zIndex: 9998 }}
|
||||
onClick={() => setShowDocumentPopover(false)}
|
||||
aria-label="Close document picker"
|
||||
/>
|
||||
{/* Popover positioned above input */}
|
||||
<div
|
||||
className="fixed shadow-2xl rounded-lg border border-border overflow-hidden"
|
||||
style={{
|
||||
zIndex: 9999,
|
||||
backgroundColor: "#18181b",
|
||||
bottom: inputRef.current ? `${window.innerHeight - inputRef.current.getBoundingClientRect().top + 8}px` : "200px",
|
||||
left: inputRef.current ? `${inputRef.current.getBoundingClientRect().left}px` : "50%",
|
||||
}}
|
||||
>
|
||||
<DocumentsDataTable
|
||||
ref={documentPickerRef}
|
||||
searchSpaceId={Number(search_space_id)}
|
||||
onSelectionChange={handleDocumentsMention}
|
||||
onDone={() => {
|
||||
setShowDocumentPopover(false);
|
||||
setMentionQuery("");
|
||||
}}
|
||||
initialSelectedDocuments={mentionedDocuments}
|
||||
externalSearch={mentionQuery}
|
||||
{showDocumentPopover &&
|
||||
typeof document !== "undefined" &&
|
||||
createPortal(
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 cursor-default"
|
||||
style={{ zIndex: 9998 }}
|
||||
onClick={() => setShowDocumentPopover(false)}
|
||||
aria-label="Close document picker"
|
||||
/>
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
{/* Popover positioned above input */}
|
||||
<div
|
||||
className="fixed shadow-2xl rounded-lg border border-border overflow-hidden"
|
||||
style={{
|
||||
zIndex: 9999,
|
||||
backgroundColor: "#18181b",
|
||||
bottom: inputRef.current
|
||||
? `${window.innerHeight - inputRef.current.getBoundingClientRect().top + 8}px`
|
||||
: "200px",
|
||||
left: inputRef.current
|
||||
? `${inputRef.current.getBoundingClientRect().left}px`
|
||||
: "50%",
|
||||
}}
|
||||
>
|
||||
<DocumentsDataTable
|
||||
ref={documentPickerRef}
|
||||
searchSpaceId={Number(search_space_id)}
|
||||
onSelectionChange={handleDocumentsMention}
|
||||
onDone={() => {
|
||||
setShowDocumentPopover(false);
|
||||
setMentionQuery("");
|
||||
}}
|
||||
initialSelectedDocuments={mentionedDocuments}
|
||||
externalSearch={mentionQuery}
|
||||
/>
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
<ComposerAction />
|
||||
</ComposerPrimitive.AttachmentDropzone>
|
||||
</ComposerPrimitive.Root>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,15 @@
|
|||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FileText } from "lucide-react";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { Document } from "@/contracts/types/document.types";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
|
|
@ -33,13 +41,16 @@ function useDebounced<T>(value: T, delay = 300) {
|
|||
}
|
||||
|
||||
export const DocumentsDataTable = forwardRef<DocumentsDataTableRef, DocumentsDataTableProps>(
|
||||
function DocumentsDataTable({
|
||||
searchSpaceId,
|
||||
onSelectionChange,
|
||||
onDone,
|
||||
initialSelectedDocuments = [],
|
||||
externalSearch = "",
|
||||
}, ref) {
|
||||
function DocumentsDataTable(
|
||||
{
|
||||
searchSpaceId,
|
||||
onSelectionChange,
|
||||
onDone,
|
||||
initialSelectedDocuments = [],
|
||||
externalSearch = "",
|
||||
},
|
||||
ref
|
||||
) {
|
||||
// Use external search
|
||||
const search = externalSearch;
|
||||
const debouncedSearch = useDebounced(search, 150);
|
||||
|
|
@ -97,10 +108,13 @@ export const DocumentsDataTable = forwardRef<DocumentsDataTableRef, DocumentsDat
|
|||
[actualDocuments, selectedIds]
|
||||
);
|
||||
|
||||
const handleSelectDocument = useCallback((doc: Document) => {
|
||||
onSelectionChange([...initialSelectedDocuments, doc]);
|
||||
onDone();
|
||||
}, [initialSelectedDocuments, onSelectionChange, onDone]);
|
||||
const handleSelectDocument = useCallback(
|
||||
(doc: Document) => {
|
||||
onSelectionChange([...initialSelectedDocuments, doc]);
|
||||
onDone();
|
||||
},
|
||||
[initialSelectedDocuments, onSelectionChange, onDone]
|
||||
);
|
||||
|
||||
// Scroll highlighted item into view
|
||||
useEffect(() => {
|
||||
|
|
@ -120,56 +134,55 @@ export const DocumentsDataTable = forwardRef<DocumentsDataTableRef, DocumentsDat
|
|||
}
|
||||
|
||||
// Expose methods to parent via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
selectHighlighted: () => {
|
||||
if (selectableDocuments[highlightedIndex]) {
|
||||
handleSelectDocument(selectableDocuments[highlightedIndex]);
|
||||
}
|
||||
},
|
||||
moveUp: () => {
|
||||
setHighlightedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : selectableDocuments.length - 1
|
||||
);
|
||||
},
|
||||
moveDown: () => {
|
||||
setHighlightedIndex((prev) =>
|
||||
prev < selectableDocuments.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
},
|
||||
}), [selectableDocuments, highlightedIndex, handleSelectDocument]);
|
||||
|
||||
// Handle keyboard navigation
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (selectableDocuments.length === 0) return;
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
prev < selectableDocuments.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : selectableDocuments.length - 1
|
||||
);
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
selectHighlighted: () => {
|
||||
if (selectableDocuments[highlightedIndex]) {
|
||||
handleSelectDocument(selectableDocuments[highlightedIndex]);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
break;
|
||||
}
|
||||
}, [selectableDocuments, highlightedIndex, handleSelectDocument, onDone]);
|
||||
},
|
||||
moveUp: () => {
|
||||
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : selectableDocuments.length - 1));
|
||||
},
|
||||
moveDown: () => {
|
||||
setHighlightedIndex((prev) => (prev < selectableDocuments.length - 1 ? prev + 1 : 0));
|
||||
},
|
||||
}),
|
||||
[selectableDocuments, highlightedIndex, handleSelectDocument]
|
||||
);
|
||||
|
||||
// Handle keyboard navigation
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (selectableDocuments.length === 0) return;
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) => (prev < selectableDocuments.length - 1 ? prev + 1 : 0));
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : selectableDocuments.length - 1));
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
if (selectableDocuments[highlightedIndex]) {
|
||||
handleSelectDocument(selectableDocuments[highlightedIndex]);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[selectableDocuments, highlightedIndex, handleSelectDocument, onDone]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className="flex flex-col w-[280px] sm:w-[320px] bg-zinc-900 rounded-lg"
|
||||
onKeyDown={handleKeyDown}
|
||||
role="listbox"
|
||||
|
|
@ -192,7 +205,7 @@ export const DocumentsDataTable = forwardRef<DocumentsDataTableRef, DocumentsDat
|
|||
const isAlreadySelected = selectedIds.has(doc.id);
|
||||
const selectableIndex = selectableDocuments.findIndex((d) => d.id === doc.id);
|
||||
const isHighlighted = !isAlreadySelected && selectableIndex === highlightedIndex;
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
key={doc.id}
|
||||
|
|
@ -211,9 +224,7 @@ export const DocumentsDataTable = forwardRef<DocumentsDataTableRef, DocumentsDat
|
|||
disabled={isAlreadySelected}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-3 py-2 text-left transition-colors",
|
||||
isAlreadySelected
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "cursor-pointer",
|
||||
isAlreadySelected ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
|
||||
isHighlighted && "bg-accent"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue