refactor(ai-chat): restructure mention handling and improve type definitions

Refactored the MentionNode and related types to enhance clarity and maintainability. Removed the old convertMentionElement function and updated the importDOM method to throw an error for unimplemented spans. Introduced a base interface for mention data and specific interfaces for different mention types, improving type safety. Updated mention text value utilities to include a new function for display text, enhancing the mention display logic.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-10-20 21:24:49 +08:00
parent 414ab5232f
commit e008dfa016
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
6 changed files with 85 additions and 45 deletions

View File

@ -1,7 +1,6 @@
import i18next from "i18next"
import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
LexicalEditor,
@ -66,14 +65,8 @@ export class MentionNode extends DecoratorNode<React.JSX.Element> {
static override importDOM(): DOMConversionMap | null {
return {
span: (domNode: HTMLElement) => {
if (!Object.hasOwn(domNode.dataset, "lexicalMention")) {
return null
}
return {
conversion: convertMentionElement,
priority: 1,
}
span: () => {
throw new Error("Not implemented")
},
}
}
@ -157,29 +150,6 @@ export class MentionNode extends DecoratorNode<React.JSX.Element> {
}
}
function convertMentionElement(domNode: HTMLElement): DOMConversionOutput {
const mentionType = domNode.dataset.mentionType as MentionData["type"] | null
const { mentionId } = domNode.dataset
const textContent = domNode.textContent || ""
if (!mentionType || !mentionId) {
return { node: null }
}
// Extract name from text content (remove @ prefix)
const name = textContent.startsWith("@") ? textContent.slice(1) : textContent
const mentionData: MentionData = {
id: mentionId,
name,
type: mentionType,
value: null,
}
const node = $createMentionNode(mentionData)
return { node }
}
export function $createMentionNode(mentionData: MentionData): MentionNode {
const mentionNode = new MentionNode(mentionData)
return $applyNodeReplacement(mentionNode)

View File

@ -22,7 +22,7 @@ import {
} from "../hooks/dateMentionUtils"
import { $isMentionNode } from "../MentionNode"
import type { MentionData } from "../types"
import { getMentionTextValue } from "../utils/mentionTextValue"
import { getMentionDisplayTextValue } from "../utils/mentionTextValue"
import { MentionTypeIcon } from "./shared/MentionTypeIcon"
interface MentionComponentProps {
@ -33,7 +33,7 @@ interface MentionComponentProps {
}
const MentionTooltipContent = ({ mentionData }: { mentionData: MentionData }) => {
const displayValue = getMentionTextValue(mentionData)
const displayValue = getMentionDisplayTextValue(mentionData)
const getIconBgColor = () => {
if (mentionData.type === "view" && typeof mentionData.value === "number") {
@ -60,7 +60,7 @@ const MentionTooltipContent = ({ mentionData }: { mentionData: MentionData }) =>
}
return (
<div className="flex items-center gap-2 p-1">
<div className="flex items-start gap-2 p-1">
<div
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded text-white",

View File

@ -4,7 +4,7 @@ import type { TFunction } from "i18next"
import { MENTION_DATE_VALUE_FORMAT } from "~/modules/ai-chat/utils/mentionDate"
import type { MentionData, MentionLabelDescriptor, MentionLabelValue } from "../types"
import type { DateMentionData, MentionLabelDescriptor, MentionLabelValue } from "../types"
import type { RelativeDateDefinition } from "./dateMentionConfig"
import { RELATIVE_DATE_DEFINITIONS } from "./dateMentionConfig"
@ -123,12 +123,12 @@ export const createDateMentionData = ({
id?: string
range: DateRange
label?: MentionLabelDescriptor
labelOptions?: MentionData["labelOptions"]
labelOptions?: DateMentionData["labelOptions"]
translate: LabelTranslator
locale: string
withRangeKey: I18nKeysForAi
displayName?: string
}): MentionData => {
}): DateMentionData => {
const value = formatRangeValue(range)
const baseLabel = displayName ?? resolveMentionLabel(label, translate) ?? value
let resolvedName = baseLabel
@ -170,7 +170,7 @@ export const parseRangeValue = (value: string): DateRange | null => {
}
export const getDateMentionDisplayName = (
mention: Pick<MentionData, "label" | "labelOptions" | "value" | "name">,
mention: Pick<DateMentionData, "label" | "labelOptions" | "value" | "name">,
translate: LabelTranslator,
locale: string,
withRangeKey: I18nKeysForAi,

View File

@ -43,7 +43,7 @@ export const useMentionSearchService = () => {
pushResult({
id: item.id,
name: item.title,
type: item.type as MentionType,
type: item.type,
value: item.id,
}),
)
@ -77,7 +77,7 @@ export const useMentionSearchService = () => {
pushResult({
id: item.id,
name: item.title,
type: item.type as MentionType,
type: item.type,
value: item.id,
}),
)

View File

@ -1,4 +1,4 @@
export type MentionType = "entry" | "feed" | "date" | "category" | "view" | "shortcut"
import type { FeedViewType } from "@follow-app/client-sdk"
export type MentionLabelValue = string | number | boolean | MentionLabelDescriptor
@ -7,17 +7,55 @@ export interface MentionLabelDescriptor {
values?: Record<string, MentionLabelValue>
}
export interface MentionData {
export interface MentionBaseData {
id: string
name: string
type: MentionType
value: unknown
label?: MentionLabelDescriptor
}
export interface EntryMentionData extends MentionBaseData {
type: "entry"
value: string
}
export interface FeedMentionData extends MentionBaseData {
type: "feed"
value: string
}
export interface DateMentionData extends MentionBaseData {
type: "date"
value: string
labelOptions?: {
appendRange?: boolean
}
}
export interface CategoryMentionData extends MentionBaseData {
type: "category"
value: string
}
export interface ShortcutMentionData extends MentionBaseData {
type: "shortcut"
value: string
}
export interface ViewMentionData extends MentionBaseData {
type: "view"
value: FeedViewType
}
export type MentionData =
| EntryMentionData
| FeedMentionData
| DateMentionData
| CategoryMentionData
| ShortcutMentionData
| ViewMentionData
export type MentionType = MentionData["type"]
export interface MentionMatch {
leadOffset: number
matchingString: string

View File

@ -1,7 +1,10 @@
import { getView } from "@follow/constants"
import { getFeedById } from "@follow/store/feed/getter"
import { getCategoryFeedIds } from "@follow/store/subscription/getter"
import { ROUTE_FEED_IN_FOLDER } from "~/constants"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { getI18n } from "~/i18n"
import type { MentionData } from "../types"
@ -20,3 +23,32 @@ export function getMentionTextValue(mentionData: MentionData): string {
return `<mention-${type} id="${value}"></mention-${type}>`
}
export function getMentionDisplayTextValue(mentionData: MentionData): string {
const { type, value } = mentionData
switch (type) {
case "category": {
if (typeof value === "string" && value.startsWith(ROUTE_FEED_IN_FOLDER)) {
const { view } = getRouteParams()
const ids = getCategoryFeedIds(value.slice(ROUTE_FEED_IN_FOLDER.length), view)
const feedNames = ids.map((id) => getFeedById(id)?.title).join(", ")
return feedNames
}
return "Unknown Category"
}
case "view": {
const viewDef = getView(value)
const viewKey = viewDef?.name
if (viewKey) {
return getI18n().t(viewKey, { ns: "common" })
}
return "Unknown View"
}
default: {
return value
}
}
}