File size: 2,742 Bytes
3201ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import React from "react";
import { useTranslation } from "react-i18next";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { SyntaxHighlighter } from "../../../markdown/syntax-highlighter";
import { CopyableContentWrapper } from "#/components/shared/buttons/copyable-content-wrapper";
import { MAX_CONTENT_LENGTH } from "#/components/conversation-events/chat/event-content-helpers/shared";
import { I18nKey } from "#/i18n/declaration";

interface CodeBlockProps {
  code: string;
  /** Prism language hint (e.g. "bash", "python"). */
  language?: string;
  /** Show a copy button on hover. Defaults to true. */
  copy?: boolean;
  /** Text shown when code is empty. */
  placeholder?: string;
  /** Let truncated content be expanded inline. Defaults to false. */
  expandable?: boolean;
  /** Wrap long lines instead of horizontal-only scrolling. Defaults to false. */
  wrapLongLines?: boolean;
}

/**
 * Syntax-highlighted code block with an optional hover copy button. Long
 * content is truncated to the same limit the markdown path uses, optionally
 * with an inline expand control. The copy button always yields the full,
 * untruncated text.
 */
export function CodeBlock({
  code,
  language,
  copy = true,
  placeholder,
  expandable = false,
  wrapLongLines = false,
}: CodeBlockProps) {
  const { t } = useTranslation("openhands");
  const [isExpanded, setIsExpanded] = React.useState(false);
  const isTruncated = code.length > MAX_CONTENT_LENGTH;
  const display =
    isTruncated && !(expandable && isExpanded)
      ? `${code.slice(0, MAX_CONTENT_LENGTH)}…`
      : code;
  const text = display.trim() || placeholder || "";
  const canCopy = copy && code.trim().length > 0;
  const toggleLabel = isExpanded
    ? t(I18nKey.BUTTON$COLLAPSE)
    : t(I18nKey.BUTTON$EXPAND);

  const block = (
    <SyntaxHighlighter
      className="rounded-lg text-xs"
      style={vscDarkPlus}
      language={language}
      PreTag="div"
      wrapLongLines={wrapLongLines}
      customStyle={wrapLongLines ? { whiteSpace: "pre-wrap" } : undefined}
      codeTagProps={
        wrapLongLines ? { style: { whiteSpace: "pre-wrap" } } : undefined
      }
    >
      {text}
    </SyntaxHighlighter>
  );

  return (
    <div className="flex flex-col gap-1">
      {canCopy ? (
        <CopyableContentWrapper text={code}>{block}</CopyableContentWrapper>
      ) : (
        block
      )}
      {expandable && isTruncated && (
        <button
          type="button"
          onClick={() => setIsExpanded((prev) => !prev)}
          className="self-start text-xs text-muted transition-colors hover:text-white hover:underline"
        >
          {toggleLabel}
        </button>
      )}
    </div>
  );
}