fix: formatting

This commit is contained in:
CREDO23 2025-12-24 07:06:35 +02:00
parent 96bf469a1f
commit deec8c5c6c
5 changed files with 183 additions and 127 deletions

View file

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

View file

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

View file

@ -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[]>>({});

View file

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

View file

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