mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
WaveAI (#93)
This commit is contained in:
parent
75c9e211d9
commit
1f973b3fdc
@ -5,6 +5,7 @@ import { CodeEdit } from "@/app/view/codeedit";
|
||||
import { PlotView } from "@/app/view/plotview";
|
||||
import { PreviewView } from "@/app/view/preview";
|
||||
import { TerminalView } from "@/app/view/term/term";
|
||||
import { WaveAi } from "@/app/view/waveai";
|
||||
import { WebView } from "@/app/view/webview";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
@ -378,6 +379,9 @@ function blockViewToIcon(view: string): string {
|
||||
if (view == "web") {
|
||||
return "globe";
|
||||
}
|
||||
if (view == "waveai") {
|
||||
return "sparkles";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -451,6 +455,8 @@ const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
||||
blockElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
||||
} else if (blockData.view === "web") {
|
||||
blockElem = <WebView key={blockId} parentRef={blockRef} initialUrl={blockData.meta.url} />;
|
||||
} else if (blockData.view === "waveai") {
|
||||
blockElem = <WaveAi key={blockId} parentRef={blockRef} />;
|
||||
}
|
||||
return (
|
||||
<BlockFrame
|
||||
|
10
frontend/app/element/copybutton.less
Normal file
10
frontend/app/element/copybutton.less
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.copy-button {
|
||||
padding: 5px 5px;
|
||||
|
||||
.fa-check {
|
||||
color: var(--app-success-color);
|
||||
}
|
||||
}
|
53
frontend/app/element/copybutton.tsx
Normal file
53
frontend/app/element/copybutton.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import * as React from "react";
|
||||
import { Button } from "./button";
|
||||
|
||||
import "./copybutton.less";
|
||||
|
||||
type CopyButtonProps = {
|
||||
title: string;
|
||||
className?: string;
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
const CopyButton = ({ title, className, onClick }: CopyButtonProps) => {
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isCopied) {
|
||||
return;
|
||||
}
|
||||
setIsCopied(true);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
timeoutRef.current = null;
|
||||
}, 2000);
|
||||
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button onClick={handleOnClick} className={clsx("copy-button secondary ghost", className)} title={title}>
|
||||
{isCopied ? <i className="fa-sharp fa-solid fa-check"></i> : <i className="fa-sharp fa-solid fa-copy"></i>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export { CopyButton };
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.markdown {
|
||||
color: var(--main-text-color);
|
||||
color: var(--app-text-color);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
@ -12,13 +12,13 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--main-text-color);
|
||||
color: var(--app-text-color);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--main-text-color);
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
table {
|
||||
tr th {
|
||||
color: var(--main-text-color);
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,30 +45,74 @@
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--panel-bg-color);
|
||||
background-color: var(--markdown-bg-color);
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--panel-bg-color);
|
||||
margin: 4px 10px 4px 10px;
|
||||
padding: 0.7em;
|
||||
pre.codeblock {
|
||||
background-color: var(--markdown-bg-color);
|
||||
margin: 4px 10px;
|
||||
padding: 0.4em 0.7em;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.codeblock-actions {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
background-color: var(--line-actions-bg-color);
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
i {
|
||||
color: var(--line-actions-inactive-color);
|
||||
margin-left: 4px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--line-actions-active-color);
|
||||
}
|
||||
|
||||
&.fa-check {
|
||||
color: var(--app-success-color);
|
||||
}
|
||||
|
||||
&.fa-square-terminal {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .codeblock-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font: var(--fixed-font);
|
||||
color: var(--main-text-color);
|
||||
color: var(--app-text-color);
|
||||
background-color: var(--markdown-bg-color);
|
||||
font-family: var(--termfontfamily);
|
||||
border-radius: 4px;
|
||||
background-color: var(--panel-bg-color);
|
||||
padding: 0.15em 0.5em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
pre.selected {
|
||||
outline: 2px solid var(--markdown-outline-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -100,6 +144,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.markdown.content {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
@ -1,43 +1,102 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { CopyButton } from "@/app/element/copybutton";
|
||||
import { clsx } from "clsx";
|
||||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import "./markdown.less";
|
||||
|
||||
function LinkRenderer(props: any): any {
|
||||
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||
const Link = ({ href, children }: { href: string; children: React.ReactNode }) => {
|
||||
const newUrl = "https://extern?" + encodeURIComponent(href);
|
||||
return (
|
||||
<a href={newUrl} target="_blank" rel={"noopener"}>
|
||||
{props.children}
|
||||
<a href={newUrl} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function HeaderRenderer(props: any, hnum: number): any {
|
||||
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
|
||||
}
|
||||
const Header = ({ children, hnum }: { children: React.ReactNode; hnum: number }) => {
|
||||
return <div className={clsx("title", `is-${hnum}`)}>{children}</div>;
|
||||
};
|
||||
|
||||
function Markdown(props: { text: string; style?: any; extraClassName?: string; codeSelect?: boolean }) {
|
||||
let text = props.text;
|
||||
let markdownComponents = {
|
||||
a: LinkRenderer,
|
||||
h1: (props) => HeaderRenderer(props, 1),
|
||||
h2: (props) => HeaderRenderer(props, 2),
|
||||
h3: (props) => HeaderRenderer(props, 3),
|
||||
h4: (props) => HeaderRenderer(props, 4),
|
||||
h5: (props) => HeaderRenderer(props, 5),
|
||||
h6: (props) => HeaderRenderer(props, 6),
|
||||
const Code = ({ children }: { children: React.ReactNode }) => {
|
||||
return <code>{children}</code>;
|
||||
};
|
||||
|
||||
type CodeBlockProps = {
|
||||
children: React.ReactNode;
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
};
|
||||
|
||||
const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
|
||||
const getTextContent = (children: any): string => {
|
||||
if (typeof children === "string") {
|
||||
return children;
|
||||
} else if (Array.isArray(children)) {
|
||||
return children.map(getTextContent).join("");
|
||||
} else if (children.props && children.props.children) {
|
||||
return getTextContent(children.props.children);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
let textToCopy = getTextContent(children);
|
||||
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
};
|
||||
|
||||
const handleExecute = (e: React.MouseEvent) => {
|
||||
let textToCopy = getTextContent(children);
|
||||
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
|
||||
if (onClickExecute) {
|
||||
onClickExecute(textToCopy);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("markdown", props.extraClassName)} style={props.style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
<pre className="codeblock">
|
||||
{children}
|
||||
<div className="codeblock-actions">
|
||||
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
|
||||
{onClickExecute && <i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>}
|
||||
</div>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
type MarkdownProps = {
|
||||
text: string;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
};
|
||||
|
||||
const Markdown = ({ text, style, className, onClickExecute }: MarkdownProps) => {
|
||||
const markdownComponents = {
|
||||
a: Link,
|
||||
h1: (props: any) => <Header {...props} hnum={1} />,
|
||||
h2: (props: any) => <Header {...props} hnum={2} />,
|
||||
h3: (props: any) => <Header {...props} hnum={3} />,
|
||||
h4: (props: any) => <Header {...props} hnum={4} />,
|
||||
h5: (props: any) => <Header {...props} hnum={5} />,
|
||||
h6: (props: any) => <Header {...props} hnum={6} />,
|
||||
code: Code,
|
||||
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("markdown content", className)} style={style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={markdownComponents}>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { Markdown };
|
||||
|
43
frontend/app/element/typingindicator.less
Normal file
43
frontend/app/element/typingindicator.less
Normal file
@ -0,0 +1,43 @@
|
||||
@dot-width: 11px;
|
||||
@dot-color: var(--app-success-color);
|
||||
@speed: 1.5s;
|
||||
|
||||
.typing {
|
||||
position: relative;
|
||||
height: @dot-width;
|
||||
|
||||
span {
|
||||
content: "";
|
||||
animation: blink @speed infinite;
|
||||
animation-fill-mode: both;
|
||||
height: @dot-width;
|
||||
width: @dot-width;
|
||||
background: @dot-color;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
margin-left: @dot-width * 1.5;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
margin-left: @dot-width * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
18
frontend/app/element/typingindicator.tsx
Normal file
18
frontend/app/element/typingindicator.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { clsx } from "clsx";
|
||||
|
||||
import "./typingindicator.less";
|
||||
|
||||
type TypingIndicatorProps = {
|
||||
className?: string;
|
||||
};
|
||||
const TypingIndicator = ({ className }: TypingIndicatorProps) => {
|
||||
return (
|
||||
<div className={clsx("typing", className)}>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { TypingIndicator };
|
@ -1,39 +0,0 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { getApi } from "./global";
|
||||
|
||||
class NavigateModelType {
|
||||
handlers: Map<string, () => void> = new Map(); // id -> handler
|
||||
urls: string[] = [];
|
||||
|
||||
constructor() {
|
||||
getApi().onNavigate(this.handleNavigate.bind(this));
|
||||
getApi().onIframeNavigate(this.handleIframeNavigate.bind(this));
|
||||
}
|
||||
|
||||
handleContextMenuClick(e: any, id: string): void {
|
||||
let handler = this.handlers.get(id);
|
||||
if (handler) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
handleNavigate(url: string): void {
|
||||
console.log("Navigate to", url);
|
||||
this.urls.push(url);
|
||||
}
|
||||
|
||||
handleIframeNavigate(url: string): void {
|
||||
console.log("Iframe navigate to", url);
|
||||
this.urls.push(url);
|
||||
}
|
||||
|
||||
getUrls(): string[] {
|
||||
return this.urls;
|
||||
}
|
||||
}
|
||||
|
||||
const NavigateModel = new NavigateModelType();
|
||||
|
||||
export { NavigateModel, NavigateModelType };
|
99
frontend/app/store/waveai.ts
Normal file
99
frontend/app/store/waveai.ts
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
interface ChatMessageType {
|
||||
id: string;
|
||||
user: string;
|
||||
text: string;
|
||||
isAssistant: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const defaultMessage: ChatMessageType = {
|
||||
id: uuidv4(),
|
||||
user: "assistant",
|
||||
text: `<p>Hello, how may I help you with this command?<br>
|
||||
(Cmd-Shift-Space: open/close, Ctrl+L: clear chat buffer, Up/Down: select code blocks, Enter: to copy a selected code block to the command input)</p>`,
|
||||
isAssistant: true,
|
||||
};
|
||||
|
||||
const messagesAtom = atom<ChatMessageType[]>([defaultMessage]);
|
||||
|
||||
const addMessageAtom = atom(null, (get, set, message: ChatMessageType) => {
|
||||
const messages = get(messagesAtom);
|
||||
set(messagesAtom, [...messages, message]);
|
||||
});
|
||||
|
||||
const updateLastMessageAtom = atom(null, (get, set, text: string) => {
|
||||
const messages = get(messagesAtom);
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage.isAssistant && !lastMessage.error) {
|
||||
const updatedMessage = { ...lastMessage, text: lastMessage.text + text };
|
||||
set(messagesAtom, [...messages.slice(0, -1), updatedMessage]);
|
||||
}
|
||||
});
|
||||
|
||||
const simulateAssistantResponseAtom = atom(null, (get, set, userMessage: ChatMessageType) => {
|
||||
const responseText = `Here is an example of a simple bash script:
|
||||
|
||||
\`\`\`bash
|
||||
#!/bin/bash
|
||||
# This is a comment
|
||||
echo "Hello, World!"
|
||||
\`\`\`
|
||||
|
||||
You can run this script by saving it to a file, for example, \`hello.sh\`, and then running \`chmod +x hello.sh\` to make it executable. Finally, run it with \`./hello.sh\`.`;
|
||||
|
||||
const typingMessage: ChatMessageType = {
|
||||
id: uuidv4(),
|
||||
user: "assistant",
|
||||
text: "",
|
||||
isAssistant: true,
|
||||
};
|
||||
|
||||
// Add a typing indicator
|
||||
set(addMessageAtom, typingMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
const parts = responseText.split(" ");
|
||||
let currentPart = 0;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (currentPart < parts.length) {
|
||||
const part = parts[currentPart] + " ";
|
||||
set(updateLastMessageAtom, part);
|
||||
currentPart++;
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, 100);
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
const useWaveAi = () => {
|
||||
const [messages] = useAtom(messagesAtom);
|
||||
const [, addMessage] = useAtom(addMessageAtom);
|
||||
const [, simulateResponse] = useAtom(simulateAssistantResponseAtom);
|
||||
|
||||
const sendMessage = (text: string, user: string = "user") => {
|
||||
const newMessage: ChatMessageType = {
|
||||
id: uuidv4(),
|
||||
user,
|
||||
text,
|
||||
isAssistant: false,
|
||||
};
|
||||
addMessage(newMessage);
|
||||
simulateResponse(newMessage);
|
||||
};
|
||||
|
||||
return {
|
||||
messages,
|
||||
sendMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export { useWaveAi };
|
||||
export type { ChatMessageType };
|
@ -2,6 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
:root {
|
||||
/* base fonts */
|
||||
--markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji";
|
||||
|
||||
--main-text-color: #f7f7f7;
|
||||
--title-font-size: 18px;
|
||||
--secondary-text-color: rgb(195, 200, 194);
|
||||
@ -31,8 +35,29 @@
|
||||
--zindex-termstickers: 20;
|
||||
|
||||
/* scrollbar colors */
|
||||
--app-text-primary-color: rgb(255, 255, 255);
|
||||
--scrollbar-background-color: var(--app-bg-color);
|
||||
--scrollbar-thumb-color: rgba(255, 255, 255, 0.3);
|
||||
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
|
||||
--scrollbar-thumb-active-color: rgba(255, 255, 255, 0.6);
|
||||
|
||||
/* global colors */
|
||||
--app-text-color: rgb(211, 215, 207);
|
||||
--app-border-color: rgb(51, 51, 51);
|
||||
--app-panel-bg-color-dev: rgb(21, 23, 48);
|
||||
--app-panel-bg-color: rgba(21, 23, 21, 1);
|
||||
--app-accent-color: rgb(88, 193, 66);
|
||||
|
||||
/* global status colors */
|
||||
--app-success-color: rgb(78, 154, 6);
|
||||
|
||||
/* form element colors */
|
||||
--form-element-primary-color: var(--app-accent-color);
|
||||
|
||||
/* markdown colors */
|
||||
--markdown-bg-color: rgb(35, 35, 35);
|
||||
--markdown-outline-color: var(--form-element-primary-color);
|
||||
|
||||
/* cmdinput colors */
|
||||
--cmdinput-text-error-color: var(--term-red);
|
||||
}
|
||||
|
93
frontend/app/view/waveai.less
Normal file
93
frontend/app/view/waveai.less
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.waveai {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
> .scrollable {
|
||||
flex-flow: column nowrap;
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.chat-window {
|
||||
display: flex;
|
||||
// margin-bottom: 5px;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
// This is the filler that will push the chat messages to the bottom until the chat window is full
|
||||
.filler {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> * {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.chat-msg-header {
|
||||
display: flex;
|
||||
margin-bottom: 2px;
|
||||
|
||||
i {
|
||||
margin-top: 2px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg-assistant {
|
||||
color: var(--app-text-color);
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg-error {
|
||||
color: var(--cmdinput-text-error);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.waveai-input-wrapper {
|
||||
padding: 16px 15px 7px;
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid var(--app-border-color);
|
||||
|
||||
.waveai-input {
|
||||
color: var(--app-text-primary-color);
|
||||
background-color: var(--cmdinput-textarea-bg);
|
||||
resize: none;
|
||||
width: 100%;
|
||||
border: transparent;
|
||||
outline: none;
|
||||
overflow: auto;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--termfontfamily);
|
||||
font-weight: normal;
|
||||
line-height: var(--termlineheight);
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
}
|
387
frontend/app/view/waveai.tsx
Normal file
387
frontend/app/view/waveai.tsx
Normal file
@ -0,0 +1,387 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/app/element/markdown";
|
||||
import { TypingIndicator } from "@/app/element/typingindicator";
|
||||
import { getApi } from "@/app/store/global";
|
||||
import { ChatMessageType, useWaveAi } from "@/app/store/waveai";
|
||||
import type { OverlayScrollbars } from "overlayscrollbars";
|
||||
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
import "./waveai.less";
|
||||
|
||||
const outline = "2px solid var(--markdown-outline-color)";
|
||||
|
||||
interface ChatItemProps {
|
||||
chatItem: ChatMessageType;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
const ChatItem = ({ chatItem, itemCount }: ChatItemProps) => {
|
||||
const { isAssistant, text, error } = chatItem;
|
||||
const senderClassName = isAssistant ? "chat-msg-assistant" : "chat-msg-user";
|
||||
const msgClassName = `chat-msg ${senderClassName}`;
|
||||
const cssVar = getApi().isDev ? "--app-panel-bg-color-dev" : "--app-panel-bg-color";
|
||||
const panelBgColor = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
|
||||
const color = tinycolor(panelBgColor);
|
||||
const newColor = color.isValid() ? tinycolor(panelBgColor).darken(6).toString() : "none";
|
||||
const backgroundColor = itemCount % 2 === 0 ? "none" : newColor;
|
||||
|
||||
const renderError = (err: string): React.JSX.Element => <div className="chat-msg-error">{err}</div>;
|
||||
|
||||
const renderContent = (): React.JSX.Element => {
|
||||
if (isAssistant) {
|
||||
if (error) {
|
||||
return renderError(error);
|
||||
}
|
||||
return text ? (
|
||||
<>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-sparkles"></i>
|
||||
</div>
|
||||
<Markdown text={text} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-sparkles"></i>
|
||||
</div>
|
||||
<TypingIndicator className="typing-indicator" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-user"></i>
|
||||
</div>
|
||||
<Markdown className="msg-text" text={text} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={msgClassName} style={{ backgroundColor }}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChatWindowProps {
|
||||
chatWindowRef: React.RefObject<HTMLDivElement>;
|
||||
messages: ChatMessageType[];
|
||||
}
|
||||
|
||||
const ChatWindow = forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(({ chatWindowRef, messages }, ref) => {
|
||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||
|
||||
useImperativeHandle(ref, () => osRef.current as OverlayScrollbarsComponentRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (osRef.current && osRef.current.osInstance()) {
|
||||
const { viewport } = osRef.current.osInstance().elements();
|
||||
viewport.scrollTo({
|
||||
behavior: "auto",
|
||||
top: chatWindowRef.current?.scrollHeight || 0,
|
||||
});
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (osRef.current && osRef.current.osInstance()) {
|
||||
osRef.current.osInstance().destroy();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleScrollbarInitialized = (instance: OverlayScrollbars) => {
|
||||
const { viewport } = instance.elements();
|
||||
viewport.scrollTo({
|
||||
behavior: "auto",
|
||||
top: chatWindowRef.current?.scrollHeight || 0,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
ref={osRef}
|
||||
className="scrollable"
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
events={{ initialized: handleScrollbarInitialized }}
|
||||
>
|
||||
<div ref={chatWindowRef} className="chat-window">
|
||||
<div className="filler"></div>
|
||||
{messages.map((chitem, idx) => (
|
||||
<ChatItem key={idx} chatItem={chitem} itemCount={idx + 1} />
|
||||
))}
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
);
|
||||
});
|
||||
|
||||
interface ChatInputProps {
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onMouseDown: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
|
||||
termFontSize: number;
|
||||
}
|
||||
|
||||
const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
||||
({ value, onChange, onKeyDown, onMouseDown, termFontSize }, ref) => {
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => textAreaRef.current as HTMLTextAreaElement);
|
||||
|
||||
useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const adjustTextAreaHeight = () => {
|
||||
if (textAreaRef.current == null) {
|
||||
return;
|
||||
}
|
||||
// Adjust the height of the textarea to fit the text
|
||||
const textAreaMaxLines = 100;
|
||||
const textAreaLineHeight = termFontSize * 1.5;
|
||||
const textAreaMinHeight = textAreaLineHeight;
|
||||
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
|
||||
|
||||
textAreaRef.current.style.height = "1px";
|
||||
const scrollHeight = textAreaRef.current.scrollHeight;
|
||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||
textAreaRef.current.style.height = newHeight + "px";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
adjustTextAreaHeight();
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="waveai-input"
|
||||
onMouseDown={onMouseDown} // When the user clicks on the textarea
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
style={{ fontSize: termFontSize }}
|
||||
placeholder="Send a Message..."
|
||||
value={value}
|
||||
></textarea>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface WaveAiProps {
|
||||
parentRef: React.MutableRefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const WaveAi = React.memo(({ parentRef }: WaveAiProps) => {
|
||||
const { messages, sendMessage } = useWaveAi();
|
||||
const waveaiRef = useRef<HTMLDivElement>(null);
|
||||
const chatWindowRef = useRef<HTMLDivElement>(null);
|
||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
const [waveAiHeight, setWaveAiHeight] = useState(0);
|
||||
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
|
||||
|
||||
const termFontSize: number = 14;
|
||||
|
||||
useEffect(() => {
|
||||
const parentElement = parentRef.current;
|
||||
setWaveAiHeight(parentElement?.getBoundingClientRect().height);
|
||||
|
||||
// Use ResizeObserver to observe changes in the height of parentRef
|
||||
const handleResize = () => {
|
||||
const webviewHeight = parentElement?.getBoundingClientRect().height;
|
||||
setWaveAiHeight(webviewHeight);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
if (entry.target === parentElement) {
|
||||
handleResize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(parentElement);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const submit = (messageStr: string) => {
|
||||
sendMessage(messageStr);
|
||||
};
|
||||
|
||||
const handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const updatePreTagOutline = (clickedPre?: HTMLElement | null) => {
|
||||
const pres = chatWindowRef.current?.querySelectorAll("pre");
|
||||
if (!pres) return;
|
||||
|
||||
pres.forEach((preElement, idx) => {
|
||||
if (preElement === clickedPre) {
|
||||
setSelectedBlockIdx(idx);
|
||||
} else {
|
||||
preElement.style.outline = "none";
|
||||
}
|
||||
});
|
||||
|
||||
if (clickedPre) {
|
||||
clickedPre.style.outline = outline;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedBlockIdx !== null) {
|
||||
const pres = chatWindowRef.current?.querySelectorAll("pre");
|
||||
if (pres && pres[selectedBlockIdx]) {
|
||||
pres[selectedBlockIdx].style.outline = outline;
|
||||
}
|
||||
}
|
||||
}, [selectedBlockIdx]);
|
||||
|
||||
const handleTextAreaMouseDown = () => {
|
||||
updatePreTagOutline();
|
||||
setSelectedBlockIdx(null);
|
||||
};
|
||||
|
||||
const handleEnterKeyPressed = () => {
|
||||
submit(value);
|
||||
setValue("");
|
||||
setSelectedBlockIdx(null);
|
||||
};
|
||||
|
||||
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
inputRef.current?.focus();
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.closest(".copy-button") ||
|
||||
target.closest(".fa-square-terminal") ||
|
||||
target.closest(".waveai-input")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pre = target.closest("pre");
|
||||
updatePreTagOutline(pre);
|
||||
};
|
||||
|
||||
const updateScrollTop = () => {
|
||||
const pres = chatWindowRef.current?.querySelectorAll("pre");
|
||||
if (!pres || selectedBlockIdx === null) return;
|
||||
|
||||
const block = pres[selectedBlockIdx];
|
||||
if (!block || !osRef.current?.osInstance()) return;
|
||||
|
||||
const { viewport, scrollOffsetElement } = osRef.current?.osInstance().elements();
|
||||
const chatWindowTop = scrollOffsetElement.scrollTop;
|
||||
const chatWindowHeight = chatWindowRef.current.clientHeight;
|
||||
const chatWindowBottom = chatWindowTop + chatWindowHeight;
|
||||
const elemTop = block.offsetTop;
|
||||
const elemBottom = elemTop + block.offsetHeight;
|
||||
const elementIsInView = elemBottom <= chatWindowBottom && elemTop >= chatWindowTop;
|
||||
|
||||
if (!elementIsInView) {
|
||||
let scrollPosition;
|
||||
if (elemBottom > chatWindowBottom) {
|
||||
scrollPosition = elemTop - chatWindowHeight + block.offsetHeight + 15;
|
||||
} else if (elemTop < chatWindowTop) {
|
||||
scrollPosition = elemTop - 15;
|
||||
}
|
||||
viewport.scrollTo({
|
||||
behavior: "auto",
|
||||
top: scrollPosition,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const shouldSelectCodeBlock = (key: "ArrowUp" | "ArrowDown") => {
|
||||
const textarea = inputRef.current;
|
||||
const cursorPosition = textarea?.selectionStart || 0;
|
||||
const textBeforeCursor = textarea?.value.slice(0, cursorPosition) || "";
|
||||
|
||||
return (
|
||||
(textBeforeCursor.indexOf("\n") === -1 && cursorPosition === 0 && key === "ArrowUp") ||
|
||||
selectedBlockIdx !== null
|
||||
);
|
||||
};
|
||||
|
||||
const handleArrowUpPressed = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (shouldSelectCodeBlock("ArrowUp")) {
|
||||
e.preventDefault();
|
||||
const pres = chatWindowRef.current?.querySelectorAll("pre");
|
||||
let blockIndex = selectedBlockIdx;
|
||||
if (!pres) return;
|
||||
if (blockIndex === null) {
|
||||
setSelectedBlockIdx(pres.length - 1);
|
||||
} else if (blockIndex > 0) {
|
||||
blockIndex--;
|
||||
setSelectedBlockIdx(blockIndex);
|
||||
}
|
||||
updateScrollTop();
|
||||
}
|
||||
};
|
||||
|
||||
const handleArrowDownPressed = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (shouldSelectCodeBlock("ArrowDown")) {
|
||||
e.preventDefault();
|
||||
const pres = chatWindowRef.current?.querySelectorAll("pre");
|
||||
let blockIndex = selectedBlockIdx;
|
||||
if (!pres) return;
|
||||
if (blockIndex === null) return;
|
||||
if (blockIndex < pres.length - 1 && blockIndex >= 0) {
|
||||
setSelectedBlockIdx(++blockIndex);
|
||||
updateScrollTop();
|
||||
} else {
|
||||
inputRef.current.focus();
|
||||
setSelectedBlockIdx(null);
|
||||
}
|
||||
updateScrollTop();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextAreaKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleEnterKeyPressed();
|
||||
} else if (e.key === "ArrowUp") {
|
||||
handleArrowUpPressed(e);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
handleArrowDownPressed(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={waveaiRef} className="waveai" onClick={handleContainerClick} style={{ height: waveAiHeight - 27 }}>
|
||||
<ChatWindow ref={osRef} chatWindowRef={chatWindowRef} messages={messages} />
|
||||
<div className="waveai-input-wrapper">
|
||||
<ChatInput
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onChange={handleTextAreaChange}
|
||||
onKeyDown={handleTextAreaKeyDown}
|
||||
onMouseDown={handleTextAreaMouseDown}
|
||||
termFontSize={termFontSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { WaveAi };
|
@ -4,7 +4,7 @@
|
||||
import { Button } from "@/app/element/button";
|
||||
import { getApi } from "@/app/store/global";
|
||||
import { WebviewTag } from "electron";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
|
||||
import "./webview.less";
|
||||
|
||||
@ -13,7 +13,7 @@ interface WebViewProps {
|
||||
initialUrl: string;
|
||||
}
|
||||
|
||||
const WebView = ({ parentRef, initialUrl }: WebViewProps) => {
|
||||
const WebView = memo(({ parentRef, initialUrl }: WebViewProps) => {
|
||||
const [url, setUrl] = useState(initialUrl);
|
||||
const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field
|
||||
const [webViewHeight, setWebViewHeight] = useState(0);
|
||||
@ -313,6 +313,6 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => {
|
||||
></webview>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export { WebView };
|
||||
|
@ -28,6 +28,7 @@
|
||||
"@types/papaparse": "^5",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/throttle-debounce": "^5",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/coverage-istanbul": "^1.6.0",
|
||||
@ -69,6 +70,7 @@
|
||||
"jotai": "^2.8.0",
|
||||
"monaco-editor": "^0.49.0",
|
||||
"overlayscrollbars": "^2.8.3",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
@ -77,9 +79,11 @@
|
||||
"react-frame-component": "^5.2.7",
|
||||
"react-gauge-chart": "^0.5.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"use-device-pixel-ratio": "^1.1.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
|
@ -89,6 +89,13 @@ func getSettingsConfigDefaults() SettingsConfigType {
|
||||
Meta: map[string]any{"url": "https://waveterm.dev/"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Icon: "sparkles",
|
||||
Label: "waveai",
|
||||
BlockDef: wstore.BlockDef{
|
||||
View: "waveai",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
153
yarn.lock
153
yarn.lock
@ -4447,6 +4447,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tinycolor2@npm:^1":
|
||||
version: 1.4.6
|
||||
resolution: "@types/tinycolor2@npm:1.4.6"
|
||||
checksum: 10c0/922020c3326460e9d8502c8a98f80db69f06fd14e07fe5a48e8ffe66175762298a9bd51263f2a0c9a40632886a74975a3ff79396defcdbeac0dc176e3e5056e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
|
||||
version: 3.0.2
|
||||
resolution: "@types/unist@npm:3.0.2"
|
||||
@ -6551,6 +6558,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"entities@npm:^4.4.0":
|
||||
version: 4.5.0
|
||||
resolution: "entities@npm:4.5.0"
|
||||
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"env-paths@npm:^2.2.0":
|
||||
version: 2.2.1
|
||||
resolution: "env-paths@npm:2.2.1"
|
||||
@ -7916,6 +7930,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-from-parse5@npm:^8.0.0":
|
||||
version: 8.0.1
|
||||
resolution: "hast-util-from-parse5@npm:8.0.1"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
hastscript: "npm:^8.0.0"
|
||||
property-information: "npm:^6.0.0"
|
||||
vfile: "npm:^6.0.0"
|
||||
vfile-location: "npm:^5.0.0"
|
||||
web-namespaces: "npm:^2.0.0"
|
||||
checksum: 10c0/4a30bb885cff1f0e023c429ae3ece73fe4b03386f07234bf23f5555ca087c2573ff4e551035b417ed7615bde559f394cdaf1db2b91c3b7f0575f3563cd238969
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-heading-rank@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "hast-util-heading-rank@npm:3.0.0"
|
||||
@ -7934,6 +7964,36 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-parse-selector@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "hast-util-parse-selector@npm:4.0.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
checksum: 10c0/5e98168cb44470dc274aabf1a28317e4feb09b1eaf7a48bbaa8c1de1b43a89cd195cb1284e535698e658e3ec26ad91bc5e52c9563c36feb75abbc68aaf68fb9f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-raw@npm:^9.0.0":
|
||||
version: 9.0.4
|
||||
resolution: "hast-util-raw@npm:9.0.4"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
"@ungap/structured-clone": "npm:^1.0.0"
|
||||
hast-util-from-parse5: "npm:^8.0.0"
|
||||
hast-util-to-parse5: "npm:^8.0.0"
|
||||
html-void-elements: "npm:^3.0.0"
|
||||
mdast-util-to-hast: "npm:^13.0.0"
|
||||
parse5: "npm:^7.0.0"
|
||||
unist-util-position: "npm:^5.0.0"
|
||||
unist-util-visit: "npm:^5.0.0"
|
||||
vfile: "npm:^6.0.0"
|
||||
web-namespaces: "npm:^2.0.0"
|
||||
zwitch: "npm:^2.0.0"
|
||||
checksum: 10c0/03d0fe7ba8bd75c9ce81f829650b19b78917bbe31db70d36bf6f136842496c3474e3bb1841f2d30dafe1f6b561a89a524185492b9a93d40b131000743c0d7998
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-to-jsx-runtime@npm:^2.0.0":
|
||||
version: 2.3.0
|
||||
resolution: "hast-util-to-jsx-runtime@npm:2.3.0"
|
||||
@ -7957,6 +8017,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-to-parse5@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "hast-util-to-parse5@npm:8.0.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
comma-separated-tokens: "npm:^2.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
property-information: "npm:^6.0.0"
|
||||
space-separated-tokens: "npm:^2.0.0"
|
||||
web-namespaces: "npm:^2.0.0"
|
||||
zwitch: "npm:^2.0.0"
|
||||
checksum: 10c0/3c0c7fba026e0c4be4675daf7277f9ff22ae6da801435f1b7104f7740de5422576f1c025023c7b3df1d0a161e13a04c6ab8f98ada96eb50adb287b537849a2bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-to-string@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "hast-util-to-string@npm:3.0.0"
|
||||
@ -7975,6 +8050,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hastscript@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "hastscript@npm:8.0.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
comma-separated-tokens: "npm:^2.0.0"
|
||||
hast-util-parse-selector: "npm:^4.0.0"
|
||||
property-information: "npm:^6.0.0"
|
||||
space-separated-tokens: "npm:^2.0.0"
|
||||
checksum: 10c0/f0b54bbdd710854b71c0f044612db0fe1b5e4d74fa2001633dc8c535c26033269f04f536f9fd5b03f234de1111808f9e230e9d19493bf919432bb24d541719e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hoist-non-react-statics@npm:^3.3.2":
|
||||
version: 3.3.2
|
||||
resolution: "hoist-non-react-statics@npm:3.3.2"
|
||||
@ -8019,6 +8107,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-void-elements@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "html-void-elements@npm:3.0.0"
|
||||
checksum: 10c0/a8b9ec5db23b7c8053876dad73a0336183e6162bf6d2677376d8b38d654fdc59ba74fdd12f8812688f7db6fad451210c91b300e472afc0909224e0a44c8610d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "http-cache-semantics@npm:4.1.1"
|
||||
@ -10347,6 +10442,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"overlayscrollbars-react@npm:^0.5.6":
|
||||
version: 0.5.6
|
||||
resolution: "overlayscrollbars-react@npm:0.5.6"
|
||||
peerDependencies:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: ">=16.8.0"
|
||||
checksum: 10c0/59a2aad3664d81dc4dee5e747b72bb2645047e0e6d300d9583244167d1e4b19c4cc263932e4b112d5c24358430f78d15d34ea389beb8845e3b5319ccc57db8d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"overlayscrollbars@npm:^2.8.3":
|
||||
version: 2.8.3
|
||||
resolution: "overlayscrollbars@npm:2.8.3"
|
||||
@ -10489,6 +10594,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse5@npm:^7.0.0":
|
||||
version: 7.1.2
|
||||
resolution: "parse5@npm:7.1.2"
|
||||
dependencies:
|
||||
entities: "npm:^4.4.0"
|
||||
checksum: 10c0/297d7af8224f4b5cb7f6617ecdae98eeaed7f8cbd78956c42785e230505d5a4f07cef352af10d3006fa5c1544b76b57784d3a22d861ae071bbc460c649482bf4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parseurl@npm:~1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "parseurl@npm:1.3.3"
|
||||
@ -11376,6 +11490,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rehype-raw@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "rehype-raw@npm:7.0.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
hast-util-raw: "npm:^9.0.0"
|
||||
vfile: "npm:^6.0.0"
|
||||
checksum: 10c0/1435b4b6640a5bc3abe3b2133885c4dbff5ef2190ef9cfe09d6a63f74dd7d7ffd0cede70603278560ccf1acbfb9da9faae4b68065a28bc5aa88ad18e40f32d52
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rehype-slug@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "rehype-slug@npm:6.0.0"
|
||||
@ -12327,6 +12452,7 @@ __metadata:
|
||||
"@types/papaparse": "npm:^5"
|
||||
"@types/react": "npm:^18.3.2"
|
||||
"@types/throttle-debounce": "npm:^5"
|
||||
"@types/tinycolor2": "npm:^1"
|
||||
"@types/uuid": "npm:^9.0.8"
|
||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||
"@vitest/coverage-istanbul": "npm:^1.6.0"
|
||||
@ -12346,6 +12472,7 @@ __metadata:
|
||||
less: "npm:^4.2.0"
|
||||
monaco-editor: "npm:^0.49.0"
|
||||
overlayscrollbars: "npm:^2.8.3"
|
||||
overlayscrollbars-react: "npm:^0.5.6"
|
||||
papaparse: "npm:^5.4.1"
|
||||
prettier: "npm:^3.2.5"
|
||||
prettier-plugin-jsdoc: "npm:^1.3.0"
|
||||
@ -12357,10 +12484,12 @@ __metadata:
|
||||
react-frame-component: "npm:^5.2.7"
|
||||
react-gauge-chart: "npm:^0.5.1"
|
||||
react-markdown: "npm:^9.0.1"
|
||||
rehype-raw: "npm:^7.0.0"
|
||||
remark-gfm: "npm:^4.0.0"
|
||||
rxjs: "npm:^7.8.1"
|
||||
storybook: "npm:^8.1.10"
|
||||
throttle-debounce: "npm:^5.0.0"
|
||||
tinycolor2: "npm:^1.6.0"
|
||||
ts-node: "npm:^10.9.2"
|
||||
tslib: "npm:^2.6.2"
|
||||
tsx: "npm:^4.15.4"
|
||||
@ -12406,6 +12535,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinycolor2@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "tinycolor2@npm:1.6.0"
|
||||
checksum: 10c0/9aa79a36ba2c2a87cb221453465cabacd04b9e35f9694373e846fdc78b1c768110f81e581ea41440106c0f24d9a023891d0887e8075885e790ac40eb0e74a5c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinypool@npm:^0.8.3":
|
||||
version: 0.8.4
|
||||
resolution: "tinypool@npm:0.8.4"
|
||||
@ -13008,6 +13144,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vfile-location@npm:^5.0.0":
|
||||
version: 5.0.2
|
||||
resolution: "vfile-location@npm:5.0.2"
|
||||
dependencies:
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
vfile: "npm:^6.0.0"
|
||||
checksum: 10c0/cfc7e49de93ac5be6f3c9a9fe77676756e00d33a6c69d9c1ce279b06eedafa67fe5d0da2334b40e97963c43b014501bca2f829dfd6622a3290fb6f7dd2b9339e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vfile-message@npm:^4.0.0":
|
||||
version: 4.0.2
|
||||
resolution: "vfile-message@npm:4.0.2"
|
||||
@ -13183,6 +13329,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-namespaces@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "web-namespaces@npm:2.0.1"
|
||||
checksum: 10c0/df245f466ad83bd5cd80bfffc1674c7f64b7b84d1de0e4d2c0934fb0782e0a599164e7197a4bce310ee3342fd61817b8047ff04f076a1ce12dd470584142a4bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webidl-conversions@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "webidl-conversions@npm:3.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user