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={() => { onSelect={() => {
trackEvent("ai", "open-settings", "d2c"); trackEvent("ai", "open-settings", "d2c");
app.setOpenDialog({ app.setOpenDialog({
name: "magicSettings", name: "settings",
source: "settings", source: "settings",
tab: "diagram-to-code",
}); });
}} }}
icon={OpenAIIcon} icon={OpenAIIcon}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,7 @@ export const TTDDialogBase = withInternalFallback(
tab, tab,
...rest ...rest
}: { }: {
tab: string; tab: "text-to-diagram" | "mermaid";
} & ( } & (
| { | {
onTextSubmit(value: string): Promise<OnTestSubmitRetValue>; onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
@ -150,11 +150,19 @@ export const TTDDialogBase = withInternalFallback(
data, data,
mermaidToExcalidrawLib, mermaidToExcalidrawLib,
setError, setError,
text: generatedResponse, mermaidDefinition: generatedResponse,
}); });
trackEvent("ai", "mermaid parse success", "ttd"); trackEvent("ai", "mermaid parse success", "ttd");
saveMermaidDataToStorage(generatedResponse); saveMermaidDataToStorage(generatedResponse);
} catch (error: any) { } 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"); trackEvent("ai", "mermaid parse failed", "ttd");
setError( setError(
new Error( new Error(
@ -206,17 +214,34 @@ export const TTDDialogBase = withInternalFallback(
app.setOpenDialog(null); app.setOpenDialog(null);
}} }}
size={1200} size={1200}
title="" title={false}
{...rest} {...rest}
autofocus={false} autofocus={false}
> >
<TTDDialogTabs tab={tab}> <TTDDialogTabs dialog="ttd" tab={tab}>
{"__fallback" in rest && rest.__fallback ? ( {"__fallback" in rest && rest.__fallback ? (
<p className="dialog-mermaid-title">{t("mermaid.title")}</p> <p className="dialog-mermaid-title">{t("mermaid.title")}</p>
) : ( ) : (
<TTDDialogTabTriggers> <TTDDialogTabTriggers>
<TTDDialogTabTrigger tab="text-to-diagram"> <TTDDialogTabTrigger tab="text-to-diagram">
<div style={{ display: "flex", alignItems: "center" }}>
{t("labels.textToDiagram")} {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>
<TTDDialogTabTrigger tab="mermaid">Mermaid</TTDDialogTabTrigger> <TTDDialogTabTrigger tab="mermaid">Mermaid</TTDDialogTabTrigger>
</TTDDialogTabTriggers> </TTDDialogTabTriggers>

View File

@ -1,34 +1,60 @@
import * as RadixTabs from "@radix-ui/react-tabs"; import * as RadixTabs from "@radix-ui/react-tabs";
import { ReactNode } from "react"; import { ReactNode, useRef } from "react";
import { useExcalidrawSetAppState } from "../App"; import { useExcalidrawSetAppState } from "../App";
import { isMemberOf } from "../../utils";
const TTDDialogTabs = ({ const TTDDialogTabs = (
children, props: {
tab,
...rest
}: {
children: ReactNode; children: ReactNode;
tab: string; } & (
}) => { | { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" }
| { dialog: "settings"; tab: "text-to-diagram" | "diagram-to-code" }
),
) => {
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const rootRef = useRef<HTMLDivElement>(null);
const minHeightRef = useRef<number>(0);
return ( return (
<RadixTabs.Root <RadixTabs.Root
ref={rootRef}
className="ttd-dialog-tabs-root" className="ttd-dialog-tabs-root"
value={tab} value={props.tab}
onValueChange={( onValueChange={(
// at least in test enviros, `tab` can be `undefined` // at least in test enviros, `tab` can be `undefined`
tab: string | 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({ 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> </RadixTabs.Root>
); );
}; };

View File

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

View File

@ -652,6 +652,19 @@
--button-bg: var(--color-surface-high); --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 { .ErrorSplash.excalidraw {
@ -735,8 +748,4 @@
letter-spacing: 0.6px; letter-spacing: 0.6px;
font-family: "Assistant"; font-family: "Assistant";
} }
.excalidraw__paragraph {
margin: 1rem 0;
}
} }

View File

@ -248,13 +248,14 @@ export interface AppState {
| null | null
| { name: "imageExport" | "help" | "jsonExport" } | { name: "imageExport" | "help" | "jsonExport" }
| { | {
name: "magicSettings"; name: "settings";
source: source:
| "tool" // when magicframe tool is selected | "tool" // when magicframe tool is selected
| "generation" // when magicframe generate button is clicked | "generation" // when magicframe generate button is clicked
| "settings"; // when AI settings dialog is explicitly invoked | "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. * Reflects user preference for whether the default sidebar should be docked.
* *