File size: 5,068 Bytes
9b906ea | 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | import { useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { ArrowUpRight } from "lucide-react";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { Typography } from "#/ui/typography";
import { I18nKey } from "#/i18n/declaration";
import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
import { useHandleBuildPlanClick } from "#/hooks/use-handle-build-plan-click";
import { cn } from "#/utils/utils";
import { useSelectConversationTab } from "#/hooks/use-select-conversation-tab";
import {
planComponents,
createPlanComponents,
} from "#/components/features/markdown/plan-components";
import { useScrollContext } from "#/context/scroll-context";
const MAX_CONTENT_LENGTH = 300;
// Shine effect class for streaming text
const SHINE_TEXT_CLASS = "shine-text";
// Plan components with shine effect applied for streaming state
const shineComponents = createPlanComponents(SHINE_TEXT_CLASS);
interface PlanPreviewProps {
/** Raw plan content from PLAN.md file */
planContent?: string | null;
/** Whether the plan content is actively being streamed */
isStreaming?: boolean;
/** Whether the Build button should be disabled (e.g., while streaming) */
isBuildDisabled?: boolean;
}
/* eslint-disable i18next/no-literal-string */
export function PlanPreview({
planContent,
isStreaming,
isBuildDisabled,
}: PlanPreviewProps) {
const { t } = useTranslation("openhands");
const { navigateToTab } = useSelectConversationTab();
const { handleBuildPlanClick } = useHandleBuildPlanClick();
const { scrollDomToBottom } = useScrollContext();
const handleViewClick = () => {
navigateToTab("planner");
};
// Handle Build action with scroll to bottom
const handleBuildClick = useCallback(
(event?: React.MouseEvent<HTMLButtonElement>) => {
handleBuildPlanClick(event);
scrollDomToBottom();
},
[handleBuildPlanClick, scrollDomToBottom],
);
// Truncate plan content for preview
const truncatedContent = useMemo(() => {
if (!planContent) return "";
if (planContent.length <= MAX_CONTENT_LENGTH) return planContent;
return `${planContent.slice(0, MAX_CONTENT_LENGTH)}...`;
}, [planContent]);
if (!planContent) {
return null;
}
return (
<div className="bg-[var(--oh-surface)] border border-[#597FF4] rounded-[12px] w-full mt-2">
{/* Header */}
<div className="border-b border-[var(--oh-border)] flex h-[41px] items-center px-2 gap-1">
<LessonPlanIcon width={18} height={18} color="var(--oh-muted)" />
<Typography.Text className="font-normal text-[11px] text-white tracking-[0.11px] leading-4">
{t(I18nKey.COMMON$PLAN_MD)}
</Typography.Text>
<div className="flex-1" />
<button
type="button"
onClick={handleViewClick}
className="flex items-center gap-1 hover:opacity-80 transition-opacity cursor-pointer"
data-testid="plan-preview-view-button"
>
<Typography.Text className="font-normal text-[11px] text-white tracking-[0.11px] leading-4">
{t(I18nKey.COMMON$VIEW)}
</Typography.Text>
<ArrowUpRight className="text-white" size={18} />
</button>
</div>
{/* Content */}
<div
data-testid="plan-preview-content"
className="flex flex-col gap-[10px] p-4 text-[15px] text-white leading-[29px]"
>
{truncatedContent && (
<>
<MarkdownRenderer
includeStandard
components={isStreaming ? shineComponents : planComponents}
>
{truncatedContent}
</MarkdownRenderer>
{planContent && planContent.length > MAX_CONTENT_LENGTH && (
<button
type="button"
onClick={handleViewClick}
className="text-[#4a67bd] cursor-pointer hover:underline text-left"
data-testid="plan-preview-read-more-button"
>
{t(I18nKey.COMMON$READ_MORE)}
</button>
)}
</>
)}
</div>
{/* Footer */}
<div className="border-t border-[var(--oh-border)] flex h-[54px] items-center justify-start px-4">
<button
type="button"
onClick={handleBuildClick}
disabled={isBuildDisabled}
className={cn(
"bg-white flex items-center justify-center h-[26px] px-2 rounded-[4px] w-[93px] transition-opacity",
isBuildDisabled
? "opacity-50 cursor-not-allowed"
: "hover:opacity-90 cursor-pointer",
)}
data-testid="plan-preview-build-button"
>
<Typography.Text className="font-normal text-[14px] text-black leading-5">
{t(I18nKey.COMMON$BUILD)}{" "}
<Typography.Text className="font-normal text-black">
⌘↩
</Typography.Text>
</Typography.Text>
</button>
</div>
</div>
);
}
|