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,75 +64,97 @@ 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
OpenAI. </h2> */}
</Paragraph> <TTDDialogTabs dialog="settings" tab={appState.openDialog.tab}>
<Paragraph> {/* <TTDDialogTabTriggers>
While the OpenAI API is in beta, its use is strictly limited as such <TTDDialogTabTrigger tab="text-to-diagram">
we require you use your own API key. You can create an{" "} <InlineIcon icon={brainIcon} /> Text to diagram
<a </TTDDialogTabTrigger>
href="https://platform.openai.com/login?launch" <TTDDialogTabTrigger tab="diagram-to-code">
rel="noopener noreferrer" <InlineIcon icon={MagicIcon} /> Wireframe to code
target="_blank" </TTDDialogTabTrigger>
</TTDDialogTabTriggers> */}
{/* <TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram">
TODO
</TTDDialogTab> */}
<TTDDialogTab
// className="ttd-dialog-content"
tab="diagram-to-code"
> >
OpenAI account <Paragraph>
</a> For the diagram-to-code feature we use{" "}
, add a small credit (5 USD minimum), and{" "} <InlineIcon icon={OpenAIIcon} />
<a OpenAI.
href="https://platform.openai.com/api-keys" </Paragraph>
rel="noopener noreferrer" <Paragraph>
target="_blank" 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{" "}
generate your own API key <a
</a> href="https://platform.openai.com/login?launch"
. rel="noopener noreferrer"
</Paragraph> target="_blank"
<Paragraph> >
Your OpenAI key does not leave the browser, and you can also set your OpenAI account
own limit in your OpenAI account dashboard if needed. </a>
</Paragraph> , add a small credit (5 USD minimum), and{" "}
<TextField <a
isRedacted href="https://platform.openai.com/api-keys"
value={keyInputValue} rel="noopener noreferrer"
placeholder="Paste your API key here" target="_blank"
label="OpenAI API key" >
onChange={(value) => { generate your own API key
setKeyInputValue(value); </a>
props.onChange(value.trim(), shouldPersist); .
}} </Paragraph>
selectOnRender <Paragraph>
onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()} Your OpenAI key does not leave the browser, and you can also set
/> your own limit in your OpenAI account dashboard if needed.
<Paragraph> </Paragraph>
By default, your API token is not persisted anywhere so you'll need to <TextField
insert it again after reload. But, you can persist locally in your isRedacted
browser below. value={keyInputValue}
</Paragraph> placeholder="Paste your API key here"
label="OpenAI API key"
onChange={(value) => {
setKeyInputValue(value);
props.onChange(value.trim(), shouldPersist);
}}
selectOnRender
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.
</Paragraph>
<CheckboxItem checked={shouldPersist} onChange={setShouldPersist}> <CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
Persist API key in browser storage Persist API key in browser storage
</CheckboxItem> </CheckboxItem>
<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
className="MagicSettings__confirm" className="MagicSettings__confirm"
size="large" size="large"
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">
{t("labels.textToDiagram")} <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>
<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, children: ReactNode;
...rest } & (
}: { | { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" }
children: ReactNode; | { dialog: "settings"; tab: "text-to-diagram" | "diagram-to-code" }
tab: string; ),
}) => { ) => {
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;
fontSize: DEFAULT_FONT_SIZE, 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); 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.
* *