You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

React Native中react-native-markdown-display组件的原生文本选择与弹窗交互实现方案咨询

React Native中react-native-markdown-display组件的原生文本选择与弹窗交互实现方案咨询

我目前正在用Expo + React Native开发一款AI聊天应用,用react-native-markdown-display来渲染LLM API返回的流式响应内容,但遇到了个头疼的问题——原生文本选择(比如复制、高亮、选择)在这个Markdown渲染组件上完全用不了

我的核心需求是:让用户能直接在聊天界面选中响应里的任意文本,选完后触发一个弹窗来分析选中的内容。

目前的临时解决方案(体验不佳)

现在我用了个折中的办法:给Markdown组件套了个TouchableOpacity,长按的时候跳转到专门的分析屏,把完整的消息文本传到新屏的TextInput里(因为TextInput支持原生选择),再在那个屏弹出分析弹窗。但这种来回切屏的体验太割裂了,我想直接在主聊天屏完成所有交互。

我需要的帮助

有没有办法实现以下目标:

  • 直接在react-native-markdown-display或者类似的Markdown渲染组件中开启原生文本选择功能?
  • 或者有没有更优的方案,能在主聊天屏直接实现「文本选择+弹窗触发」的交互,彻底去掉额外的跳转步骤?

任何相关的库、技巧或者可行的临时方案我都非常欢迎!

我的当前代码片段

HomeChatScreen.tsx

<TouchableOpacity 
  activeOpacity={1} 
  delayLongPress={300} 
  onLongPress={() => {
    // @ts-ignore
    navigation.navigate('AnalyzeScreen', {
      messageText: msg.text,
      messageId: msg.messageId,
      sessionId: sessionId
    });
  }}
>
  <ChatMarkdown 
    text={msg.text} 
    theme={theme} 
    msgId={msg.id} 
    highlights={messageHighlights} 
  />
</TouchableOpacity>

ChatMarkdown.tsx

import React from 'react';
import { StyleSheet, Text } from 'react-native';
import Markdown, { MarkdownIt } from 'react-native-markdown-display';
import CustomCodeBlock from './CustomCodeBlock';
import { getColorByName } from '../utils/highlightChatUtils';
import AnalyzeModal from './AnalyzeModal';
import type { Highlight } from '../app/api/Api';

const applyHighlights = (text: string, highlights: Highlight[] = [], theme: any) => {
  if (!highlights?.length) return text;
  const sortedHighlights = [...highlights].sort((a, b) => b.start - a.start);
  let highlightedText = text;
  sortedHighlights.forEach((highlight) => {
    const { start, end, color } = highlight;
    if (start < 0 || end > highlightedText.length || start >= end) {
      return;
    }
    const beforeText = highlightedText.substring(0, start);
    const highlightedPart = highlightedText.substring(start, end);
    const afterText = highlightedText.substring(end);
    const colorValue = getColorByName(color);
    highlightedText = `${beforeText}[[HIGHLIGHT_START:${colorValue}]]${highlightedPart}[[HIGHLIGHT_END]]${afterText}`;
  });
  return highlightedText;
};

export default function ChatMarkdown({ text, theme, msgId, highlights }: { text: string; theme: any; msgId: string; highlights?: Highlight[]; }) {
  if (!text) return null;
  const highlightedText = applyHighlights(text, highlights || [], theme);

  const rules = {
    code_block: (node: any, _children: any, _parent: any, styles: any) => (
      <CustomCodeBlock key={node.key} node={node} styles={styles} theme={theme} highlights={highlights} />
    ),
    fence: (node: any, _children: any, _parent: any, styles: any) => (
      <CustomCodeBlock key={node.key} node={node} styles={styles} theme={theme} highlights={highlights} />
    ),
    // 自定义文本处理器处理高亮
    text: (node: any, _children: any, _parent: any, styles: any) => {
      if (!node.content) return null;
      const content = node.content;
      if (content.includes('[[HIGHLIGHT_START:') && content.includes('[[HIGHLIGHT_END]]')) {
        return processHighlightedText(content, node.key, styles, theme);
      }
      return (
        <Text selectable={true} key={node.key} style={styles.body}>
          {content}
        </Text>
      );
    },
    // 让标题支持选择
    heading1: (node: any, children: any, _parent: any, styles: any) => (
      <Text selectable={true} key={node.key} style={[styles.heading1]}>
        {children}
      </Text>
    ),
    heading2: (node: any, children: any, _parent: any, styles: any) => (
      <Text selectable={true} key={node.key} style={[styles.heading2]}>
        {children}
      </Text>
    ),
    heading3: (node: any, children: any, _parent: any, styles: any) => (
      <Text selectable={true} key={node.key} style={[styles.heading3]}>
        {children}
      </Text>
    ),
    // 让列表项支持选择
    list_item: (node: any, children: any, _parent: any, styles: any) => (
      <Text selectable={true} key={node.key} style={styles.list_item}>
        • {children}
      </Text>
    ),
    // 让链接支持选择(同时保留点击功能)
    link: (node: any, children: any, _parent: any, styles: any) => (
      <Text selectable={true} key={node.key} style={styles.link} onPress={() => {
        // 处理链接点击(如果需要)
        // Linking.openURL(node.attributes.href);
      }}
      >
        {children}
      </Text>
    ),
    // 让粗体文本支持选择
    strong: (node: any, children: any, _parent: any, styles: any) => (
      <Text selectable={true} key={node.key} style={styles.strong}>
        {children}
      </Text>
    ),
  };

  return (
    <Markdown
      rules={rules}
      style={styles.markdown}
      theme={theme}
    >
      {highlightedText}
    </Markdown>
  );
}

const styles = StyleSheet.create({
  markdown: {
    // 自定义样式
  },
});

(注:我注意到在ChatMarkdown里已经给部分Text组件加了selectable={true},但因为外层套了TouchableOpacity,可能阻止了原生的选择交互?这也是我疑惑的点之一)

内容来源于stack exchange

火山引擎 最新活动