feat: TTD dialog tweaks (#7346)

* tweaks to TTD dialog ~ prepping for settings dialog

* tweaks to ttd parsing & error logging
This commit is contained in:
David Luzar 2023-11-27 16:03:03 +01:00 committed by GitHub
parent fe75f29c15
commit dd220bcaea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 227 additions and 104 deletions

View File

@ -363,8 +363,9 @@ export const ShapesSwitcher = ({
onSelect={() => {
trackEvent("ai", "open-settings", "d2c");
app.setOpenDialog({
name: "magicSettings",
name: "settings",
source: "settings",
tab: "diagram-to-code",
});
}}
icon={OpenAIIcon}

View File

@ -1700,7 +1700,11 @@ class App extends React.Component<AppProps, AppState> {
) {
if (!this.OPENAI_KEY) {
this.setState({
openDialog: { name: "magicSettings", source: "generation" },
openDialog: {
name: "settings",
tab: "diagram-to-code",
source: "generation",
},
});
trackEvent("ai", "generate (missing key)", "d2c");
return;
@ -1871,7 +1875,11 @@ class App extends React.Component<AppProps, AppState> {
public onMagicframeToolSelect = () => {
if (!this.OPENAI_KEY) {
this.setState({
openDialog: { name: "magicSettings", source: "tool" },
openDialog: {
name: "settings",
tab: "diagram-to-code",
source: "tool",
},
});
trackEvent("ai", "tool-select (missing key)", "d2c");
return;

View File

@ -461,14 +461,14 @@ const LayerUI = ({
}}
/>
)}
{appState.openDialog?.name === "magicSettings" && (
{appState.openDialog?.name === "settings" && (
<MagicSettings
openAIKey={openAIKey}
isPersisted={isOpenAIKeyPersisted}
onChange={onOpenAIAPIKeyChange}
onConfirm={(apiKey, shouldPersist) => {
const source =
appState.openDialog?.name === "magicSettings"
appState.openDialog?.name === "settings"
? appState.openDialog?.source
: "settings";
setAppState({ openDialog: null }, () => {

View File

@ -1,9 +1,18 @@
.excalidraw {
.MagicSettings {
.Island {
height: 100%;
display: flex;
flex-direction: column;
}
}
.MagicSettings-confirm {
padding: 0.5rem 1rem;
}
.MagicSettings__confirm {
margin-top: 2rem;
margin-right: auto;
}
}

View File

@ -10,6 +10,8 @@ import { InlineIcon } from "./InlineIcon";
import { Paragraph } from "./Paragraph";
import "./MagicSettings.scss";
import TTDDialogTabs from "./TTDDialog/TTDDialogTabs";
import { TTDDialogTab } from "./TTDDialog/TTDDialogTab";
export const MagicSettings = (props: {
openAIKey: string | null;
@ -18,16 +20,21 @@ export const MagicSettings = (props: {
onConfirm: (key: string, shouldPersist: boolean) => void;
onClose: () => void;
}) => {
const { theme } = useUIAppState();
const [keyInputValue, setKeyInputValue] = useState(props.openAIKey || "");
const [shouldPersist, setShouldPersist] = useState<boolean>(
props.isPersisted,
);
const appState = useUIAppState();
const onConfirm = () => {
props.onConfirm(keyInputValue.trim(), shouldPersist);
};
if (appState.openDialog?.name !== "settings") {
return null;
}
return (
<Dialog
onCloseRequest={() => {
@ -36,7 +43,7 @@ export const MagicSettings = (props: {
}}
title={
<div style={{ display: "flex" }}>
Diagram to Code (AI){" "}
Wireframe to Code (AI){" "}
<div
style={{
display: "flex",
@ -46,7 +53,8 @@ export const MagicSettings = (props: {
marginLeft: "1rem",
fontSize: 14,
borderRadius: "12px",
background: theme === "light" ? "#FFCCCC" : "#703333",
color: "#000",
background: "pink",
}}
>
Experimental
@ -56,19 +64,39 @@ export const MagicSettings = (props: {
className="MagicSettings"
autofocus={false}
>
<Paragraph
{/* <h2
style={{
display: "inline-flex",
alignItems: "center",
marginBottom: 0,
margin: 0,
fontSize: "1.25rem",
paddingLeft: "2.5rem",
}}
>
For the diagram-to-code feature we use <InlineIcon icon={OpenAIIcon} />
AI Settings
</h2> */}
<TTDDialogTabs dialog="settings" tab={appState.openDialog.tab}>
{/* <TTDDialogTabTriggers>
<TTDDialogTabTrigger tab="text-to-diagram">
<InlineIcon icon={brainIcon} /> Text to diagram
</TTDDialogTabTrigger>
<TTDDialogTabTrigger tab="diagram-to-code">
<InlineIcon icon={MagicIcon} /> Wireframe to code
</TTDDialogTabTrigger>
</TTDDialogTabTriggers> */}
{/* <TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram">
TODO
</TTDDialogTab> */}
<TTDDialogTab
// className="ttd-dialog-content"
tab="diagram-to-code"
>
<Paragraph>
For the diagram-to-code feature we use{" "}
<InlineIcon icon={OpenAIIcon} />
OpenAI.
</Paragraph>
<Paragraph>
While the OpenAI API is in beta, its use is strictly limited as such
we require you use your own API key. You can create an{" "}
While the OpenAI API is in beta, its use is strictly limited as
such we require you use your own API key. You can create an{" "}
<a
href="https://platform.openai.com/login?launch"
rel="noopener noreferrer"
@ -87,8 +115,8 @@ export const MagicSettings = (props: {
.
</Paragraph>
<Paragraph>
Your OpenAI key does not leave the browser, and you can also set your
own limit in your OpenAI account dashboard if needed.
Your OpenAI key does not leave the browser, and you can also set
your own limit in your OpenAI account dashboard if needed.
</Paragraph>
<TextField
isRedacted
@ -103,9 +131,9 @@ export const MagicSettings = (props: {
onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()}
/>
<Paragraph>
By default, your API token is not persisted anywhere so you'll need to
insert it again after reload. But, you can persist locally in your
browser below.
By default, your API token is not persisted anywhere so you'll need
to insert it again after reload. But, you can persist locally in
your browser below.
</Paragraph>
<CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
@ -114,9 +142,9 @@ export const MagicSettings = (props: {
<Paragraph>
Once API key is set, you can use the <InlineIcon icon={MagicIcon} />{" "}
tool to wrap your elements in a frame that will then allow you to turn
it into code. This dialog can be accessed using the <b>AI Settings</b>{" "}
<InlineIcon icon={OpenAIIcon} />.
tool to wrap your elements in a frame that will then allow you to
turn it into code. This dialog can be accessed using the{" "}
<b>AI Settings</b> <InlineIcon icon={OpenAIIcon} />.
</Paragraph>
<FilledButton
@ -125,6 +153,8 @@ export const MagicSettings = (props: {
label="Confirm"
onClick={onConfirm}
/>
</TTDDialogTab>
</TTDDialogTabs>
</Dialog>
);
};

View File

@ -18,8 +18,11 @@
overflow: auto;
padding: calc(var(--space-factor) * 10);
display: flex;
flex-direction: column;
.Island {
padding: 2.5rem !important;
padding: 2.5rem;
}
}

View File

@ -63,7 +63,7 @@ const MermaidToExcalidraw = ({
data,
mermaidToExcalidrawLib,
setError,
text: deferredText,
mermaidDefinition: deferredText,
}).catch(() => {});
}, [deferredText, mermaidToExcalidrawLib]);

View File

@ -72,7 +72,7 @@ export const TTDDialogBase = withInternalFallback(
tab,
...rest
}: {
tab: string;
tab: "text-to-diagram" | "mermaid";
} & (
| {
onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
@ -150,11 +150,19 @@ export const TTDDialogBase = withInternalFallback(
data,
mermaidToExcalidrawLib,
setError,
text: generatedResponse,
mermaidDefinition: generatedResponse,
});
trackEvent("ai", "mermaid parse success", "ttd");
saveMermaidDataToStorage(generatedResponse);
} catch (error: any) {
console.info(
`%cTTD mermaid render errror: ${error.message}`,
"color: red",
);
console.info(
`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\nTTD mermaid definition render errror: ${error.message}`,
"color: yellow",
);
trackEvent("ai", "mermaid parse failed", "ttd");
setError(
new Error(
@ -206,17 +214,34 @@ export const TTDDialogBase = withInternalFallback(
app.setOpenDialog(null);
}}
size={1200}
title=""
title={false}
{...rest}
autofocus={false}
>
<TTDDialogTabs tab={tab}>
<TTDDialogTabs dialog="ttd" tab={tab}>
{"__fallback" in rest && rest.__fallback ? (
<p className="dialog-mermaid-title">{t("mermaid.title")}</p>
) : (
<TTDDialogTabTriggers>
<TTDDialogTabTrigger tab="text-to-diagram">
<div style={{ display: "flex", alignItems: "center" }}>
{t("labels.textToDiagram")}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "1px 6px",
marginLeft: "10px",
fontSize: 10,
borderRadius: "12px",
background: "pink",
color: "#000",
}}
>
AI Beta
</div>
</div>
</TTDDialogTabTrigger>
<TTDDialogTabTrigger tab="mermaid">Mermaid</TTDDialogTabTrigger>
</TTDDialogTabTriggers>

View File

@ -1,34 +1,60 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { ReactNode } from "react";
import { ReactNode, useRef } from "react";
import { useExcalidrawSetAppState } from "../App";
import { isMemberOf } from "../../utils";
const TTDDialogTabs = ({
children,
tab,
...rest
}: {
const TTDDialogTabs = (
props: {
children: ReactNode;
tab: string;
}) => {
} & (
| { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" }
| { dialog: "settings"; tab: "text-to-diagram" | "diagram-to-code" }
),
) => {
const setAppState = useExcalidrawSetAppState();
const rootRef = useRef<HTMLDivElement>(null);
const minHeightRef = useRef<number>(0);
return (
<RadixTabs.Root
ref={rootRef}
className="ttd-dialog-tabs-root"
value={tab}
value={props.tab}
onValueChange={(
// at least in test enviros, `tab` can be `undefined`
tab: string | undefined,
) => {
if (tab) {
if (!tab) {
return;
}
const modalContentNode =
rootRef.current?.closest<HTMLElement>(".Modal__content");
if (modalContentNode) {
const currHeight = modalContentNode.offsetHeight || 0;
if (currHeight > minHeightRef.current) {
minHeightRef.current = currHeight;
modalContentNode.style.minHeight = `min(${minHeightRef.current}px, 100%)`;
}
}
if (
props.dialog === "settings" &&
isMemberOf(["text-to-diagram", "diagram-to-code"], tab)
) {
setAppState({
openDialog: { name: "ttd", tab },
openDialog: { name: props.dialog, tab, source: "settings" },
});
} else if (
props.dialog === "ttd" &&
isMemberOf(["text-to-diagram", "mermaid"], tab)
) {
setAppState({
openDialog: { name: props.dialog, tab },
});
}
}}
{...rest}
>
{children}
{props.children}
</RadixTabs.Root>
);
};

View File

@ -43,7 +43,7 @@ export interface MermaidToExcalidrawLibProps {
interface ConvertMermaidToExcalidrawFormatProps {
canvasRef: React.RefObject<HTMLDivElement>;
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
text: string;
mermaidDefinition: string;
setError: (error: Error | null) => void;
data: React.MutableRefObject<{
elements: readonly NonDeletedExcalidrawElement[];
@ -54,7 +54,7 @@ interface ConvertMermaidToExcalidrawFormatProps {
export const convertMermaidToExcalidraw = async ({
canvasRef,
mermaidToExcalidrawLib,
text,
mermaidDefinition,
setError,
data,
}: ConvertMermaidToExcalidrawFormatProps) => {
@ -65,7 +65,7 @@ export const convertMermaidToExcalidraw = async ({
return;
}
if (!text) {
if (!mermaidDefinition) {
resetPreview({ canvasRef, setError });
return;
}
@ -73,9 +73,20 @@ export const convertMermaidToExcalidraw = async ({
try {
const api = await mermaidToExcalidrawLib.api;
const { elements, files } = await api.parseMermaidToExcalidraw(text, {
let ret;
try {
ret = await api.parseMermaidToExcalidraw(mermaidDefinition, {
fontSize: DEFAULT_FONT_SIZE,
});
} catch (err: any) {
ret = await api.parseMermaidToExcalidraw(
mermaidDefinition.replace(/"/g, "'"),
{
fontSize: DEFAULT_FONT_SIZE,
},
);
}
const { elements, files } = ret;
setError(null);
data.current = {
@ -101,7 +112,7 @@ export const convertMermaidToExcalidraw = async ({
} catch (err: any) {
console.error(err);
parent.style.background = "var(--default-bg-color)";
if (text) {
if (mermaidDefinition) {
setError(err);
}

View File

@ -652,6 +652,19 @@
--button-bg: var(--color-surface-high);
}
}
.excalidraw__paragraph {
margin: 1rem 0;
}
.Modal__content {
.excalidraw__paragraph:first-child {
margin-top: 0;
}
.excalidraw__paragraph + .excalidraw__paragraph {
margin-top: 0rem;
}
}
}
.ErrorSplash.excalidraw {
@ -735,8 +748,4 @@
letter-spacing: 0.6px;
font-family: "Assistant";
}
.excalidraw__paragraph {
margin: 1rem 0;
}
}

View File

@ -248,13 +248,14 @@ export interface AppState {
| null
| { name: "imageExport" | "help" | "jsonExport" }
| {
name: "magicSettings";
name: "settings";
source:
| "tool" // when magicframe tool is selected
| "generation" // when magicframe generate button is clicked
| "settings"; // when AI settings dialog is explicitly invoked
tab: "text-to-diagram" | "diagram-to-code";
}
| { name: "ttd"; tab: string };
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" };
/**
* Reflects user preference for whether the default sidebar should be docked.
*