import {
ActionBarPrimitive,
AssistantIf,
BranchPickerPrimitive,
ComposerPrimitive,
ErrorPrimitive,
MessagePrimitive,
ThreadPrimitive,
useAssistantState,
} from "@assistant-ui/react";
import {
ArrowDownIcon,
ArrowUpIcon,
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
DownloadIcon,
Loader2,
PencilIcon,
RefreshCwIcon,
SquareIcon,
} from "lucide-react";
import { useParams } from "next/navigation";
import type { FC } from "react";
import { useRef, useState } from "react";
import {
ComposerAddAttachment,
ComposerAttachments,
UserMessageAttachments,
} from "@/components/assistant-ui/attachment";
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 } from "@/components/new-chat/DocumentsDataTable";
import { Button } from "@/components/ui/button";
import type { Document } from "@/contracts/types/document.types";
import { cn } from "@/lib/utils";
export const Thread: FC = () => {
return (
thread.isEmpty}>
);
};
const ThreadScrollToBottom: FC = () => {
return (
);
};
const ThreadWelcome: FC = () => {
return (
Hello there!
How can I help you today?
);
};
const SUGGESTIONS = [
{
title: "What's the weather",
label: "in San Francisco?",
prompt: "What's the weather in San Francisco?",
},
{
title: "Explain React hooks",
label: "like useState and useEffect",
prompt: "Explain React hooks like useState and useEffect",
},
] as const;
const ThreadSuggestions: FC = () => {
return (
{SUGGESTIONS.map((suggestion, index) => (
))}
);
};
const Composer: FC = () => {
// ---- State for document mentions ----
const [allSelectedDocuments, setAllSelectedDocuments] = useState([]);
const [mentionedDocuments, setMentionedDocuments] = useState([]);
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
const [inputValue, setInputValue] = useState("");
const inputRef = useRef(null);
const { search_space_id } = useParams();
const handleInputOrKeyUp = (
e: React.FormEvent | React.KeyboardEvent
) => {
const textarea = e.currentTarget;
const value = textarea.value;
setInputValue(value);
// Regex: finds all [title] occurrences
const mentionRegex = /\[([^\]]+)\]/g;
const titlesMentioned: string[] = [];
let match;
while ((match = mentionRegex.exec(value)) !== null) {
titlesMentioned.push(match[1]);
}
// Use allSelectedDocuments to filter down for current chips
setMentionedDocuments(
allSelectedDocuments.filter((doc) => titlesMentioned.includes(doc.title))
);
const selectionStart = textarea.selectionStart;
// Only open if the last character before the caret is exactly '@'
if (
selectionStart !== null &&
value[selectionStart - 1] === "@" &&
value.length === selectionStart
) {
setShowDocumentPopover(true);
} else {
setShowDocumentPopover(false);
}
};
const handleDocumentsMention = (documents: Document[]) => {
// Add newly selected docs to allSelectedDocuments
setAllSelectedDocuments((prev) => {
const toAdd = documents.filter((doc) => !prev.find((p) => p.id === doc.id));
return [...prev, ...toAdd];
});
let newValue = inputValue;
documents.forEach((doc) => {
const refString = `[${doc.title}]`;
if (!newValue.includes(refString)) {
if (newValue.trim() !== "" && !newValue.endsWith(" ")) {
newValue += " ";
}
newValue += refString;
}
});
setInputValue(newValue);
// Run the chip update as well right after change
const mentionRegex = /\[([^\]]+)\]/g;
const titlesMentioned: string[] = [];
let match;
while ((match = mentionRegex.exec(newValue)) !== null) {
titlesMentioned.push(match[1]);
}
setMentionedDocuments(
allSelectedDocuments.filter((doc) => titlesMentioned.includes(doc.title))
);
};
return (
{/* -------- Input field w/ refs and handlers -------- */}
{/* -------- Document mention popover (simple version) -------- */}
{showDocumentPopover && (
)}
);
};
const ComposerAction: FC = () => {
// Check if any attachments are still being processed (running AND progress < 100)
// When progress is 100, processing is done but waiting for send()
const hasProcessingAttachments = useAssistantState(({ composer }) =>
composer.attachments?.some((att) => {
const status = att.status;
if (status?.type !== "running") return false;
const progress = (status as { type: "running"; progress?: number }).progress;
return progress === undefined || progress < 100;
})
);
return (
{/* Show processing indicator when attachments are being processed */}
{hasProcessingAttachments && (