use-rich-text-extensions.ts5.4 KBView on GitHub import { useCallback, useMemo } from 'react';
import type { AnyExtension, Editor } from '@tiptap/core';
import type { Range } from '@tiptap/core';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import {
FileLinkNode,
createFileLinkSuggestionExtension,
useCachedFileLinkSearch,
} from '@/modules/documents/file-link';
import { ConversationNode } from '@/modules/agentCanvas/extensions/ConversationNode';
import { createConversationMention } from '@/modules/documents/mention/ConversationMention';
import type { ConversationMentionItem } from '@/modules/documents/mention/ConversationMentionList';
import { createEventMention } from '@/modules/documents/mention/EventMention';
import { EventNode } from '@/modules/documents/mention/EventNode';
import type { EventMentionItem } from '@/modules/documents/mention/EventMentionList';
import { useTRPC } from '@/providers/query-provider';
function onConversationSelect(
conversation: ConversationMentionItem,
editor: Editor,
range: Range,
) {
editor.chain().focus().deleteRange(range).run();
if (!editor.state.schema.nodes.conversationNode) {
console.warn('[useRichTextExtensions] conversationNode schema not registered');
return;
}
editor.commands.insertContent({
type: 'conversationNode',
attrs: { conversationId: conversation.id },
});
}
function onEventSelect(event: EventMentionItem, editor: Editor, range: Range) {
editor.chain().focus().deleteRange(range).run();
editor.commands.insertContent({
type: 'eventNode',
attrs: {
eventId: event.id,
eventType: event.eventType,
title: event.title,
},
});
}
export function useRichTextExtensions({
includeConversationMention = true,
}: { includeConversationMention?: boolean } = {}): AnyExtension[] {
const trpc = useTRPC();
const queryClient = useQueryClient();
useQuery(
trpc.crm.searchConversationsMinimal.queryOptions(
{ query: '', limit: 30 },
{ staleTime: 5 * 60 * 1000, gcTime: Infinity },
),
);
const searchFileLinks = useCachedFileLinkSearch();
const searchConversations = useCallback(
async (query: string): Promise<ConversationMentionItem[]> => {
type ConversationMinimal = {
id: string;
name: string | null;
companyName: string | null;
companyLogoUrl: string | null;
};
type SearchResult = { conversations: ConversationMinimal[] };
const toMentionItems = (rows: ConversationMinimal[]): ConversationMentionItem[] =>
rows.map((c) => ({
id: c.id,
name: c.name,
companyName: c.companyName,
companyLogoUrl: c.companyLogoUrl,
}));
const trimmed = query.trim();
const recentOptions = trpc.crm.searchConversationsMinimal.queryOptions(
{ query: '', limit: 30 },
{ staleTime: 5 * 60 * 1000, gcTime: Infinity },
);
if (!trimmed) {
const cachedRecent = queryClient.getQueryData<SearchResult>(recentOptions.queryKey);
if (cachedRecent?.conversations) return toMentionItems(cachedRecent.conversations);
const result = (await queryClient.fetchQuery(recentOptions)) as SearchResult;
return toMentionItems(result.conversations ?? []);
}
const searchOptions = trpc.crm.searchConversationsMinimal.queryOptions(
{ query: trimmed, limit: 10 },
{ staleTime: 30_000 },
);
const cachedSearch = queryClient.getQueryData<SearchResult>(searchOptions.queryKey);
if (cachedSearch?.conversations) return toMentionItems(cachedSearch.conversations);
const cachedRecent = queryClient.getQueryData<SearchResult>(recentOptions.queryKey);
const recentRows = cachedRecent?.conversations ?? [];
if (recentRows.length > 0) {
const q = trimmed.toLowerCase();
const filtered = recentRows.filter(
(c) =>
(c.name?.toLowerCase().includes(q) ?? false) ||
(c.companyName?.toLowerCase().includes(q) ?? false),
);
void queryClient.prefetchQuery(searchOptions);
if (filtered.length > 0) return toMentionItems(filtered.slice(0, 10));
}
const result = (await queryClient.fetchQuery(searchOptions)) as SearchResult;
return toMentionItems(result.conversations ?? []);
},
[queryClient, trpc],
);
const searchEvents = useCallback(
async (query: string): Promise<EventMentionItem[]> => {
const trimmed = query.trim();
const options = trpc.crm.searchEventsForMention.queryOptions(
{ query: trimmed, limit: 15 },
{ staleTime: 30_000 },
);
const cached = queryClient.getQueryData<{ events: EventMentionItem[] }>(options.queryKey);
if (cached?.events) return cached.events;
const result = (await queryClient.fetchQuery(options)) as { events: EventMentionItem[] };
return result.events ?? [];
},
[queryClient, trpc],
);
return useMemo<AnyExtension[]>(() => {
const extensions: AnyExtension[] = [
FileLinkNode,
createFileLinkSuggestionExtension({ search: searchFileLinks }),
];
if (includeConversationMention) {
extensions.push(
ConversationNode,
createConversationMention({ search: searchConversations, onSelect: onConversationSelect }),
);
}
extensions.push(
EventNode,
createEventMention({ search: searchEvents, onSelect: onEventSelect }),
);
return extensions;
}, [includeConversationMention, searchFileLinks, searchConversations, searchEvents]);
}