Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type JSONContent, TiptapEditor } from "@hypr/tiptap/editor";
import NoteEditor from "@hypr/tiptap/editor";
import { EMPTY_TIPTAP_DOC, isValidTiptapContent } from "@hypr/tiptap/shared";

import { useSearchEngine } from "../../../../../../contexts/search/engine";
import { useImageUpload } from "../../../../../../hooks/useImageUpload";
import * as main from "../../../../../../store/tinybase/store/main";

Expand Down Expand Up @@ -40,14 +41,21 @@ export const EnhancedEditor = forwardRef<
main.STORE_ID,
);

const { search } = useSearchEngine();

const mentionConfig = useMemo(
() => ({
trigger: "@",
handleSearch: async () => {
return [];
handleSearch: async (query: string) => {
const results = await search(query);
return results.slice(0, 5).map((hit) => ({
id: hit.document.id,
type: hit.document.type,
label: hit.document.title,
}));
},
}),
[],
[search],
);

const fileHandlerConfig = useMemo(() => ({ onImageUpload }), [onImageUpload]);
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop/src/components/main/body/sessions/note-input/raw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type PlaceholderFunction,
} from "@hypr/tiptap/shared";

import { useSearchEngine } from "../../../../../contexts/search/engine";
import { useImageUpload } from "../../../../../hooks/useImageUpload";
import * as main from "../../../../../store/tinybase/store/main";

Expand Down Expand Up @@ -73,14 +74,21 @@ export const RawEditor = forwardRef<
[persistChange, hasNonEmptyText],
);

const { search } = useSearchEngine();

const mentionConfig = useMemo(
() => ({
trigger: "@",
handleSearch: async () => {
return [];
handleSearch: async (query: string) => {
const results = await search(query);
return results.slice(0, 5).map((hit) => ({
id: hit.document.id,
type: hit.document.type,
label: hit.document.title,
}));
},
}),
[],
[search],
);

const fileHandlerConfig = useMemo(() => ({ onImageUpload }), [onImageUpload]);
Expand Down
21 changes: 15 additions & 6 deletions apps/desktop/src/contexts/search/engine/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,28 @@ export function SearchEngineProvider({
query: string,
filters: SearchFilters | null = null,
): Promise<SearchHit[]> => {
const normalizedQuery = normalizeQuery(query);

if (normalizedQuery.length < 1) {
return [];
}

if (!oramaInstance.current) {
return [];
}

const normalizedQuery = normalizeQuery(query);

try {
const whereClause = buildOramaFilters(filters);

if (normalizedQuery.length < 1) {
const searchResults = await oramaSearch(oramaInstance.current, {
term: "",
sortBy: {
property: "created_at",
order: "DESC",
},
limit: 10,
...(whereClause && { where: whereClause }),
});
return searchResults.hits as SearchHit[];
}

const searchResults = await oramaSearch(oramaInstance.current, {
term: normalizedQuery,
boost: {
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/store/tinybase/store/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ export const StoreComponent = () => {
"enhanced_notes",
"template_id",
"position",
)
.setIndexDefinition(
INDEXES.mentionsBySource,
"mapping_mention",
"source_id",
)
.setIndexDefinition(
INDEXES.mentionsByTarget,
"mapping_mention",
"target_id",
),
);

Expand Down Expand Up @@ -356,6 +366,8 @@ export const INDEXES = {
sessionsByHuman: "sessionsByHuman",
enhancedNotesBySession: "enhancedNotesBySession",
enhancedNotesByTemplate: "enhancedNotesByTemplate",
mentionsBySource: "mentionsBySource",
mentionsByTarget: "mentionsByTarget",
};

export const RELATIONSHIPS = {
Expand Down
70 changes: 70 additions & 0 deletions apps/desktop/src/utils/mentions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { MentionSourceType, MentionTargetType } from "@hypr/store";
import type { JSONContent } from "@hypr/tiptap/editor";

export interface ExtractedMention {
target_id: string;
target_type: MentionTargetType;
}

export function extractMentionsFromTiptap(
content: JSONContent,
): ExtractedMention[] {
const mentions: ExtractedMention[] = [];
const seen = new Set<string>();

const traverse = (node: JSONContent) => {
if (node.type === "mention-@" && node.attrs) {
const key = `${node.attrs.type}:${node.attrs.id}`;
if (!seen.has(key)) {
seen.add(key);
mentions.push({
target_id: node.attrs.id as string,
target_type: node.attrs.type as MentionTargetType,
});
}
}

if (node.content) {
for (const child of node.content) {
traverse(child);
}
}
};

traverse(content);
return mentions;
}

export function syncMentions(
store: {
getSliceRowIds: (indexId: string, sliceId: string) => string[];
delRow: (tableId: string, rowId: string) => void;
setRow: (
tableId: string,
rowId: string,
row: Record<string, unknown>,
) => void;
},
userId: string,
sourceId: string,
sourceType: MentionSourceType,
mentions: ExtractedMention[],
indexId: string,
) {
const existingRowIds = store.getSliceRowIds(indexId, sourceId);

for (const rowId of existingRowIds) {
store.delRow("mapping_mention", rowId);
}

for (const mention of mentions) {
const rowId = `${sourceId}:${mention.target_type}:${mention.target_id}`;
store.setRow("mapping_mention", rowId, {
user_id: userId,
source_id: sourceId,
source_type: sourceType,
target_id: mention.target_id,
target_type: mention.target_type,
});
}
}
8 changes: 8 additions & 0 deletions packages/store/src/tinybase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
eventSchema,
generalSchema,
humanSchema,
mappingMentionSchema,
mappingSessionParticipantSchema,
mappingTagSessionSchema,
organizationSchema,
Expand Down Expand Up @@ -96,6 +97,13 @@ export const tableSchemaForTinybase = {
tag_id: { type: "string" },
session_id: { type: "string" },
} as const satisfies InferTinyBaseSchema<typeof mappingTagSessionSchema>,
mapping_mention: {
user_id: { type: "string" },
source_id: { type: "string" },
source_type: { type: "string" },
target_id: { type: "string" },
target_type: { type: "string" },
} as const satisfies InferTinyBaseSchema<typeof mappingMentionSchema>,
templates: {
user_id: { type: "string" },
title: { type: "string" },
Expand Down
20 changes: 20 additions & 0 deletions packages/store/src/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,24 @@ export const mappingTagSessionSchema = z.object({
session_id: z.string(),
});

export const mentionTargetTypeSchema = z.enum([
"session",
"human",
"organization",
]);
export type MentionTargetType = z.infer<typeof mentionTargetTypeSchema>;

export const mentionSourceTypeSchema = z.enum(["session", "enhanced_note"]);
export type MentionSourceType = z.infer<typeof mentionSourceTypeSchema>;

export const mappingMentionSchema = z.object({
user_id: z.string(),
source_id: z.string(),
source_type: mentionSourceTypeSchema,
target_id: z.string(),
target_type: mentionTargetTypeSchema,
});

export const templateSectionSchema = z.object({
title: z.string(),
description: z.string(),
Expand Down Expand Up @@ -232,6 +250,7 @@ export type MappingSessionParticipant = z.infer<
>;
export type Tag = z.infer<typeof tagSchema>;
export type MappingTagSession = z.infer<typeof mappingTagSessionSchema>;
export type MappingMention = z.infer<typeof mappingMentionSchema>;
export type Template = z.infer<typeof templateSchema>;
export type TemplateSection = z.infer<typeof templateSectionSchema>;
export type ChatGroup = z.infer<typeof chatGroupSchema>;
Expand All @@ -257,5 +276,6 @@ export type EventStorage = ToStorageType<typeof eventSchema>;
export type MappingSessionParticipantStorage = ToStorageType<
typeof mappingSessionParticipantSchema
>;
export type MappingMentionStorage = ToStorageType<typeof mappingMentionSchema>;
export type AIProviderStorage = ToStorageType<typeof aiProviderSchema>;
export type GeneralStorage = ToStorageType<typeof generalSchema>;
9 changes: 7 additions & 2 deletions packages/tiptap/src/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import "../../styles.css";
import * as shared from "../shared";
import type { FileHandlerConfig } from "../shared/extensions";
import type { PlaceholderFunction } from "../shared/extensions/placeholder";
import { mention, type MentionConfig } from "./mention";
import { isMentionActive, mention, type MentionConfig } from "./mention";

const safeRequestIdleCallback =
typeof requestIdleCallback !== "undefined"
Expand Down Expand Up @@ -106,7 +106,12 @@ const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>(
}
}

if (event.key === "ArrowUp" && isInFirstBlock && onNavigateToTitle) {
if (
event.key === "ArrowUp" &&
isInFirstBlock &&
onNavigateToTitle &&
!isMentionActive(state)
) {
event.preventDefault();

const firstBlock = state.doc.firstChild;
Expand Down
37 changes: 27 additions & 10 deletions packages/tiptap/src/editor/mention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@ import {
type VirtualElement,
} from "@floating-ui/dom";
import Mention from "@tiptap/extension-mention";
import { PluginKey } from "@tiptap/pm/state";
import { type EditorState, PluginKey } from "@tiptap/pm/state";
import { ReactRenderer } from "@tiptap/react";
import { type SuggestionOptions } from "@tiptap/suggestion";
import { Building2Icon, StickyNoteIcon, UserIcon } from "lucide-react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";

const GLOBAL_NAVIGATE_FUNCTION = "__HYPR_NAVIGATE__";

const mentionPluginKeys: PluginKey[] = [];

export function isMentionActive(state: EditorState): boolean {
return mentionPluginKeys.some((key) => {
const pluginState = key.getState(state);
return pluginState?.active === true;
});
}

export interface MentionItem {
id: string;
type: string;
Expand Down Expand Up @@ -109,6 +119,13 @@ const Component = forwardRef<
key={item.id}
onClick={() => selectItem(index)}
>
{item.type === "session" ? (
<StickyNoteIcon className="mention-type-icon mention-type-session" />
) : item.type === "human" ? (
<UserIcon className="mention-type-icon mention-type-human" />
) : item.type === "organization" ? (
<Building2Icon className="mention-type-icon mention-type-organization" />
) : null}
<span className="mention-label">{item.label}</span>
</button>
);
Expand Down Expand Up @@ -146,9 +163,12 @@ const suggestion = (
}
};

const pluginKey = new PluginKey(`mention-${config.trigger}`);
mentionPluginKeys.push(pluginKey);

return {
char: config.trigger,
pluginKey: new PluginKey(`mention-${config.trigger}`),
pluginKey,
command: ({ editor, range, props }) => {
const item = props as MentionItem;
if (item.content) {
Expand Down Expand Up @@ -177,16 +197,13 @@ const suggestion = (
}
},
items: async ({ query }) => {
if (!query || query.length < 1) {
loading = false;
return [];
}
const normalizedQuery = query ?? "";

if (query === currentQuery && cachedItems.length > 0) {
if (normalizedQuery === currentQuery && cachedItems.length > 0) {
return cachedItems;
}

currentQuery = query;
currentQuery = normalizedQuery;

if (abortController) {
abortController.abort();
Expand All @@ -196,7 +213,7 @@ const suggestion = (
loading = true;

setTimeout(() => {
Promise.resolve(config.handleSearch(query))
Promise.resolve(config.handleSearch(normalizedQuery))
.then((items: MentionItem[]) => {
cachedItems = items.slice(0, 5);
loading = false;
Expand All @@ -221,7 +238,7 @@ const suggestion = (
const update = () => {
void computePosition(referenceEl, floatingEl, {
placement: "bottom-start",
middleware: [offset(0), flip(), shift({ limiter: limitShift() })],
middleware: [offset(4), flip(), shift({ limiter: limitShift() })],
}).then(({ x, y }) => {
Object.assign(floatingEl.style, {
left: `${x}px`,
Expand Down
Loading
Loading