mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-11-28 05:00:26 +08:00
364 lines
15 KiB
TypeScript
364 lines
15 KiB
TypeScript
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import Logo from "@/app/asset/logo.svg";
|
|
import { Button } from "@/app/element/button";
|
|
import { EmojiButton } from "@/app/element/emojibutton";
|
|
import { MagnifyIcon } from "@/app/element/magnify";
|
|
import { atoms, globalStore } from "@/app/store/global";
|
|
import * as WOS from "@/app/store/wos";
|
|
import { RpcApi } from "@/app/store/wshclientapi";
|
|
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
|
import { isMacOS } from "@/util/platformutil";
|
|
import { useEffect, useState } from "react";
|
|
import { FakeChat } from "./fakechat";
|
|
import { EditBashrcCommand, ViewLogoCommand, ViewShortcutsCommand } from "./onboarding-command";
|
|
import { CurrentOnboardingVersion } from "./onboarding-common";
|
|
import { FakeLayout } from "./onboarding-layout";
|
|
|
|
type FeaturePageName = "waveai" | "magnify" | "files";
|
|
|
|
const OnboardingFooter = ({
|
|
currentStep,
|
|
totalSteps,
|
|
onNext,
|
|
onPrev,
|
|
onSkip,
|
|
}: {
|
|
currentStep: number;
|
|
totalSteps: number;
|
|
onNext: () => void;
|
|
onPrev?: () => void;
|
|
onSkip?: () => void;
|
|
}) => {
|
|
const isLastStep = currentStep === totalSteps;
|
|
const buttonText = isLastStep ? "Get Started" : "Next";
|
|
|
|
return (
|
|
<footer className="unselectable flex-shrink-0 mt-5 relative">
|
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
|
{currentStep > 1 && onPrev && (
|
|
<button className="text-muted cursor-pointer hover:text-foreground text-[13px]" onClick={onPrev}>
|
|
< Prev
|
|
</button>
|
|
)}
|
|
<span className="text-muted text-[13px]">
|
|
{currentStep} of {totalSteps}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-row items-center justify-center [&>button]:!px-5 [&>button]:!py-2 [&>button]:text-sm">
|
|
<Button className="font-[600]" onClick={onNext}>
|
|
{buttonText}
|
|
</Button>
|
|
</div>
|
|
{!isLastStep && onSkip && (
|
|
<button
|
|
className="absolute right-0 top-1/2 -translate-y-1/2 text-muted cursor-pointer hover:text-muted-hover text-[13px]"
|
|
onClick={onSkip}
|
|
>
|
|
Skip Feature Tour >
|
|
</button>
|
|
)}
|
|
</footer>
|
|
);
|
|
};
|
|
|
|
const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) => {
|
|
const isMac = isMacOS();
|
|
const shortcutKey = isMac ? "⌘-Shift-A" : "Alt-Shift-A";
|
|
const [fireClicked, setFireClicked] = useState(false);
|
|
|
|
const handleFireClick = () => {
|
|
setFireClicked(!fireClicked);
|
|
if (!fireClicked) {
|
|
RpcApi.RecordTEventCommand(TabRpcClient, {
|
|
event: "onboarding:fire",
|
|
props: {
|
|
"onboarding:feature": "waveai",
|
|
"onboarding:version": CurrentOnboardingVersion,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<header className="flex items-center gap-4 mb-6 w-full unselectable flex-shrink-0">
|
|
<div>
|
|
<Logo />
|
|
</div>
|
|
<div className="text-[25px] font-normal text-foreground">Wave AI</div>
|
|
</header>
|
|
<div className="flex-1 flex flex-row gap-0 min-h-0">
|
|
<div className="flex-1 flex flex-col items-center justify-center gap-8 pr-6 unselectable">
|
|
<div className="flex flex-col items-start gap-6 max-w-md">
|
|
<div className="flex h-[52px] px-3 items-center rounded-lg bg-hover text-accent text-[24px]">
|
|
<i className="fa fa-sparkles" />
|
|
<span className="font-bold ml-2 font-mono">AI</span>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-start gap-4 text-secondary">
|
|
<p>
|
|
Wave AI is your terminal assistant with context. I can read your terminal output,
|
|
analyze widgets, read/write files, and help you solve problems faster.
|
|
</p>
|
|
|
|
<div className="flex items-start gap-3 w-full">
|
|
<i className="fa fa-sparkles text-accent text-lg mt-1 flex-shrink-0" />
|
|
<p>
|
|
Toggle the Wave AI panel with the{" "}
|
|
<span className="inline-flex h-[26px] px-1.5 items-center rounded-md box-border bg-hover text-accent text-[12px] align-middle">
|
|
<i className="fa fa-sparkles" />
|
|
<span className="font-bold ml-1 font-mono">AI</span>
|
|
</span>{" "}
|
|
button in the header (top left)
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-3 w-full">
|
|
<i className="fa fa-keyboard text-accent text-lg mt-1 flex-shrink-0" />
|
|
<p>
|
|
Or use the keyboard shortcut{" "}
|
|
<span className="font-mono font-semibold text-foreground whitespace-nowrap">
|
|
{shortcutKey}
|
|
</span>{" "}
|
|
to quickly toggle
|
|
</p>
|
|
</div>
|
|
|
|
<EmojiButton emoji="🔥" isClicked={fireClicked} onClick={handleFireClick} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="w-[2px] bg-border flex-shrink-0"></div>
|
|
<div className="flex items-center justify-center pl-6 flex-shrink-0 w-[400px]">
|
|
<div className="w-full h-[400px] bg-background rounded border border-border/50 overflow-hidden">
|
|
<FakeChat />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<OnboardingFooter currentStep={1} totalSteps={3} onNext={onNext} onSkip={onSkip} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const MagnifyBlocksPage = ({
|
|
onNext,
|
|
onSkip,
|
|
onPrev,
|
|
}: {
|
|
onNext: () => void;
|
|
onSkip: () => void;
|
|
onPrev?: () => void;
|
|
}) => {
|
|
const isMac = isMacOS();
|
|
const shortcutKey = isMac ? "⌘" : "Alt";
|
|
const [fireClicked, setFireClicked] = useState(false);
|
|
|
|
const handleFireClick = () => {
|
|
setFireClicked(!fireClicked);
|
|
if (!fireClicked) {
|
|
RpcApi.RecordTEventCommand(TabRpcClient, {
|
|
event: "onboarding:fire",
|
|
props: {
|
|
"onboarding:feature": "magnify",
|
|
"onboarding:version": CurrentOnboardingVersion,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<header className="flex items-center gap-4 mb-6 w-full unselectable flex-shrink-0">
|
|
<div>
|
|
<Logo />
|
|
</div>
|
|
<div className="text-[25px] font-normal text-foreground">Magnify Blocks</div>
|
|
</header>
|
|
<div className="flex-1 flex flex-row gap-0 min-h-0">
|
|
<div className="flex-1 flex flex-col items-center justify-center gap-8 pr-6 unselectable">
|
|
<div className="text-6xl font-semibold text-foreground">{shortcutKey}-M</div>
|
|
<div className="flex flex-col items-start gap-4 text-secondary max-w-md">
|
|
<p>
|
|
Magnify any block to focus on what matters. Expand terminals, editors, and previews for a
|
|
better view.
|
|
</p>
|
|
<p>Use the magnify feature to work with complex outputs and large files more efficiently.</p>
|
|
<p>
|
|
You can also magnify a block by clicking on the{" "}
|
|
<span className="inline-block align-middle [&_svg_path]:!fill-foreground">
|
|
<MagnifyIcon enabled={false} />
|
|
</span>{" "}
|
|
icon in the block header.
|
|
</p>
|
|
<p>
|
|
A quick {shortcutKey}-M to magnify and another {shortcutKey}-M to unmagnify
|
|
</p>
|
|
<EmojiButton emoji="🔥" isClicked={fireClicked} onClick={handleFireClick} />
|
|
</div>
|
|
</div>
|
|
<div className="w-[2px] bg-border flex-shrink-0"></div>
|
|
<div className="flex items-center justify-center pl-6 flex-shrink-0 w-[400px]">
|
|
<FakeLayout />
|
|
</div>
|
|
</div>
|
|
<OnboardingFooter currentStep={2} totalSteps={3} onNext={onNext} onPrev={onPrev} onSkip={onSkip} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?: () => void }) => {
|
|
const [fireClicked, setFireClicked] = useState(false);
|
|
const isMac = isMacOS();
|
|
const [commandIndex, setCommandIndex] = useState(0);
|
|
const [key, setKey] = useState(0);
|
|
|
|
const handleFireClick = () => {
|
|
setFireClicked(!fireClicked);
|
|
if (!fireClicked) {
|
|
RpcApi.RecordTEventCommand(TabRpcClient, {
|
|
event: "onboarding:fire",
|
|
props: {
|
|
"onboarding:feature": "wsh",
|
|
"onboarding:version": CurrentOnboardingVersion,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
const commands = [
|
|
(onComplete: () => void) => <EditBashrcCommand onComplete={onComplete} />,
|
|
(onComplete: () => void) => <ViewShortcutsCommand isMac={isMac} onComplete={onComplete} />,
|
|
(onComplete: () => void) => <ViewLogoCommand onComplete={onComplete} />,
|
|
];
|
|
|
|
const handleCommandComplete = () => {
|
|
setTimeout(() => {
|
|
setCommandIndex((prev) => (prev + 1) % commands.length);
|
|
setKey((prev) => prev + 1);
|
|
}, 2500);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<header className="flex items-center gap-4 mb-6 w-full unselectable flex-shrink-0">
|
|
<div>
|
|
<Logo />
|
|
</div>
|
|
<div className="text-[25px] font-normal text-foreground">Viewing/Editing Files</div>
|
|
</header>
|
|
<div className="flex-1 flex flex-row gap-0 min-h-0">
|
|
<div className="flex-1 flex flex-col items-center justify-center gap-8 pr-6 unselectable">
|
|
<div className="flex flex-col items-start gap-6 max-w-md">
|
|
<div className="flex flex-col items-start gap-4 text-secondary">
|
|
<p>
|
|
Wave can preview markdown, images, and video files on both local <i>and remote</i>{" "}
|
|
machines.
|
|
</p>
|
|
|
|
<div className="flex items-start gap-3 w-full">
|
|
<i className="fa fa-eye text-accent text-lg mt-1 flex-shrink-0" />
|
|
<div>
|
|
<p className="mb-2">
|
|
Use{" "}
|
|
<span className="font-mono font-semibold text-foreground">
|
|
wsh view [filename]
|
|
</span>{" "}
|
|
to preview files in Wave's graphical viewer
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-3 w-full">
|
|
<i className="fa fa-pen-to-square text-accent text-lg mt-1 flex-shrink-0" />
|
|
<div>
|
|
<p className="mb-2">
|
|
Use{" "}
|
|
<span className="font-mono font-semibold text-foreground">
|
|
wsh edit [filename]
|
|
</span>{" "}
|
|
to open config files or code files in Wave's graphical editor
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p>
|
|
These commands work seamlessly on both local and remote machines, making it easy to view
|
|
and edit files wherever they are.
|
|
</p>
|
|
|
|
<EmojiButton emoji="🔥" isClicked={fireClicked} onClick={handleFireClick} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="w-[2px] bg-border flex-shrink-0"></div>
|
|
<div className="flex items-center justify-center pl-6 flex-shrink-0 w-[400px]">
|
|
{commands[commandIndex](handleCommandComplete)}
|
|
</div>
|
|
</div>
|
|
<OnboardingFooter currentStep={3} totalSteps={3} onNext={onFinish} onPrev={onPrev} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) => {
|
|
const [currentPage, setCurrentPage] = useState<FeaturePageName>("waveai");
|
|
|
|
useEffect(() => {
|
|
const clientId = globalStore.get(atoms.clientId);
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
oref: WOS.makeORef("client", clientId),
|
|
meta: { "onboarding:lastversion": CurrentOnboardingVersion },
|
|
});
|
|
RpcApi.RecordTEventCommand(TabRpcClient, {
|
|
event: "onboarding:start",
|
|
props: {
|
|
"onboarding:version": CurrentOnboardingVersion,
|
|
},
|
|
});
|
|
}, []);
|
|
|
|
const handleNext = () => {
|
|
if (currentPage === "waveai") {
|
|
setCurrentPage("magnify");
|
|
} else if (currentPage === "magnify") {
|
|
setCurrentPage("files");
|
|
}
|
|
};
|
|
|
|
const handlePrev = () => {
|
|
if (currentPage === "magnify") {
|
|
setCurrentPage("waveai");
|
|
} else if (currentPage === "files") {
|
|
setCurrentPage("magnify");
|
|
}
|
|
};
|
|
|
|
const handleSkip = () => {
|
|
RpcApi.RecordTEventCommand(TabRpcClient, {
|
|
event: "onboarding:skip",
|
|
props: {},
|
|
});
|
|
onComplete();
|
|
};
|
|
|
|
const handleFinish = () => {
|
|
onComplete();
|
|
};
|
|
|
|
let pageComp: React.JSX.Element = null;
|
|
switch (currentPage) {
|
|
case "waveai":
|
|
pageComp = <WaveAIPage onNext={handleNext} onSkip={handleSkip} />;
|
|
break;
|
|
case "magnify":
|
|
pageComp = <MagnifyBlocksPage onNext={handleNext} onSkip={handleSkip} onPrev={handlePrev} />;
|
|
break;
|
|
case "files":
|
|
pageComp = <FilesPage onFinish={handleFinish} onPrev={handlePrev} />;
|
|
break;
|
|
}
|
|
|
|
return <div className="flex flex-col w-full h-full">{pageComp}</div>;
|
|
};
|