more work on getting the editor to cache and update properly
This commit is contained in:
@@ -26,6 +26,8 @@ model PromptVariant {
|
|||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
label String
|
label String
|
||||||
|
|
||||||
|
uiId String @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
visible Boolean @default(true)
|
visible Boolean @default(true)
|
||||||
sortIndex Int @default(0)
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
@@ -37,6 +39,8 @@ model PromptVariant {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
ModelOutput ModelOutput[]
|
ModelOutput ModelOutput[]
|
||||||
|
|
||||||
|
@@index([uiId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model TestScenario {
|
model TestScenario {
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
import { Box, Button, Stack, Title } from "@mantine/core";
|
import { Box, Button, Group, Stack, Title } from "@mantine/core";
|
||||||
import { useMonaco } from "@monaco-editor/react";
|
import { useMonaco } from "@monaco-editor/react";
|
||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState, useCallback } from "react";
|
||||||
import { set } from "zod";
|
import { set } from "zod";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
let isThemeDefined = false;
|
let isThemeDefined = false;
|
||||||
|
|
||||||
export default function VariantConfigEditor(props: {
|
export default function VariantConfigEditor(props: {
|
||||||
initialConfig: string;
|
savedConfig: string;
|
||||||
onSave: (currentConfig: string) => Promise<void>;
|
onSave: (currentConfig: string) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
||||||
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
||||||
const [isChanged, setIsChanged] = useState(false);
|
const [isChanged, setIsChanged] = useState(false);
|
||||||
|
const savedConfigRef = useRef(props.savedConfig);
|
||||||
|
|
||||||
|
const checkForChanges = useCallback(() => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
const currentConfig = editorRef.current.getValue();
|
||||||
|
setIsChanged(currentConfig !== savedConfigRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [onSave] = useHandledAsyncCallback(async () => {
|
const [onSave] = useHandledAsyncCallback(async () => {
|
||||||
const currentConfig = editorRef.current?.getValue();
|
const currentConfig = editorRef.current?.getValue();
|
||||||
if (!currentConfig) return;
|
if (!currentConfig) return;
|
||||||
await props.onSave(currentConfig);
|
await props.onSave(currentConfig);
|
||||||
}, [props.onSave]);
|
checkForChanges();
|
||||||
|
}, [props.onSave, checkForChanges]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monaco) {
|
if (monaco) {
|
||||||
@@ -36,7 +44,7 @@ export default function VariantConfigEditor(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editorRef.current = monaco.editor.create(document.getElementById(editorId) as HTMLElement, {
|
editorRef.current = monaco.editor.create(document.getElementById(editorId) as HTMLElement, {
|
||||||
value: props.initialConfig,
|
value: props.savedConfig,
|
||||||
language: "json",
|
language: "json",
|
||||||
theme: "customTheme",
|
theme: "customTheme",
|
||||||
lineNumbers: "off",
|
lineNumbers: "off",
|
||||||
@@ -50,28 +58,56 @@ export default function VariantConfigEditor(props: {
|
|||||||
wordWrapBreakBeforeCharacters: "",
|
wordWrapBreakBeforeCharacters: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
editorRef.current.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, onSave);
|
editorRef.current.onDidFocusEditorText(() => {
|
||||||
|
// Workaround because otherwise the command only works on whatever
|
||||||
editorRef.current.onDidChangeModelContent(() => {
|
// editor was loaded on the page last.
|
||||||
const currentConfig = editorRef.current?.getValue();
|
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
|
||||||
if (currentConfig !== props.initialConfig) {
|
editorRef.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, onSave);
|
||||||
setIsChanged(true);
|
|
||||||
} else {
|
|
||||||
setIsChanged(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => editorRef.current?.dispose();
|
editorRef.current.onDidChangeModelContent(checkForChanges);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [monaco, editorId, props.initialConfig, onSave]);
|
|
||||||
|
// We intentionally skip the onSave and props.savedConfig dependencies here because
|
||||||
|
// we don't want to re-render the editor from scratch
|
||||||
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
|
}, [monaco, editorId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedConfigChanged = savedConfigRef.current !== props.savedConfig;
|
||||||
|
|
||||||
|
savedConfigRef.current = props.savedConfig;
|
||||||
|
|
||||||
|
if (savedConfigChanged && editorRef.current?.getValue() !== props.savedConfig) {
|
||||||
|
editorRef.current?.setValue(props.savedConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForChanges();
|
||||||
|
}, [props.savedConfig, checkForChanges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box w="100%" pos="relative">
|
<Box w="100%" pos="relative">
|
||||||
<div id={editorId} style={{ height: "300px", width: "100%" }}></div>
|
<div id={editorId} style={{ height: "300px", width: "100%" }}></div>
|
||||||
{isChanged && (
|
{isChanged && (
|
||||||
<Button size="xs" sx={{ position: "absolute", bottom: 0, right: 0 }} onClick={onSave}>
|
<Group sx={{ position: "absolute", bottom: 0, right: 0 }} spacing={4}>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
editorRef.current?.setValue(props.savedConfig);
|
||||||
|
checkForChanges();
|
||||||
|
}}
|
||||||
|
color="gray"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button size="xs" onClick={onSave}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Button, Stack, Title } from "@mantine/core";
|
import { Stack, Title } from "@mantine/core";
|
||||||
import { useMonaco } from "@monaco-editor/react";
|
import { useCallback } from "react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import type { PromptVariant } from "./types";
|
import type { PromptVariant } from "./types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { type JSONSerializable } from "~/server/types";
|
import { type JSONSerializable } from "~/server/types";
|
||||||
import VariantConfigEditor from "./VariantConfigEditor";
|
import VariantConfigEditor from "./VariantConfigEditor";
|
||||||
@@ -44,16 +42,13 @@ export default function VariantHeader({ variant }: { variant: PromptVariant }) {
|
|||||||
|
|
||||||
// TODO: invalidate the variants query
|
// TODO: invalidate the variants query
|
||||||
},
|
},
|
||||||
[variant.id, replaceWithConfig]
|
[variant.id, replaceWithConfig, utils.promptVariants.list]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack w="100%">
|
<Stack w="100%">
|
||||||
<Title order={4}>{variant.label}</Title>
|
<Title order={4}>{variant.label}</Title>
|
||||||
<VariantConfigEditor
|
<VariantConfigEditor savedConfig={JSON.stringify(variant.config, null, 2)} onSave={onSave} />
|
||||||
initialConfig={JSON.stringify(variant.config, null, 2)}
|
|
||||||
onSave={onSave}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
);
|
);
|
||||||
|
|
||||||
const columns = useMemo<MRT_ColumnDef<TableRow>[]>(() => {
|
const columns = useMemo<MRT_ColumnDef<TableRow>[]>(() => {
|
||||||
console.log(
|
|
||||||
"rebuilding cols",
|
|
||||||
variants.data?.map((variant) => variant.label)
|
|
||||||
);
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "scenario",
|
id: "scenario",
|
||||||
@@ -47,7 +43,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
},
|
},
|
||||||
...(variants.data?.map(
|
...(variants.data?.map(
|
||||||
(variant): MRT_ColumnDef<TableRow> => ({
|
(variant): MRT_ColumnDef<TableRow> => ({
|
||||||
id: variant.id,
|
id: variant.uiId,
|
||||||
header: variant.label,
|
header: variant.label,
|
||||||
Header: <VariantHeader variant={variant} />,
|
Header: <VariantHeader variant={variant} />,
|
||||||
size: 400,
|
size: 400,
|
||||||
@@ -87,9 +83,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
enableHiding={false}
|
enableHiding={false}
|
||||||
enableColumnActions={false}
|
enableColumnActions={false}
|
||||||
enableColumnResizing
|
enableColumnResizing
|
||||||
state={{
|
|
||||||
columnOrder: ["scenario", ...variants.data.map((variant) => variant.id)],
|
|
||||||
}}
|
|
||||||
mantineTableProps={{
|
mantineTableProps={{
|
||||||
sx: {
|
sx: {
|
||||||
th: {
|
th: {
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("got existing", existing);
|
|
||||||
console.log("config", input.config);
|
|
||||||
|
|
||||||
let parsedConfig;
|
let parsedConfig;
|
||||||
try {
|
try {
|
||||||
parsedConfig = JSON.parse(input.config) as OpenAIChatConfig;
|
parsedConfig = JSON.parse(input.config) as OpenAIChatConfig;
|
||||||
@@ -44,19 +41,32 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
throw new Error(`Prompt Variant with id ${input.id} does not exist`);
|
throw new Error(`Prompt Variant with id ${input.id} does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("new config", {
|
||||||
|
experimentId: existing.experimentId,
|
||||||
|
label: existing.label,
|
||||||
|
sortIndex: existing.sortIndex,
|
||||||
|
uiId: existing.uiId,
|
||||||
|
config: parsedConfig,
|
||||||
|
});
|
||||||
|
|
||||||
// Create a duplicate with only the config changed
|
// Create a duplicate with only the config changed
|
||||||
const newVariant = await prisma.promptVariant.create({
|
const newVariant = await prisma.promptVariant.create({
|
||||||
data: {
|
data: {
|
||||||
experimentId: existing.experimentId,
|
experimentId: existing.experimentId,
|
||||||
label: existing.label,
|
label: existing.label,
|
||||||
sortIndex: existing.sortIndex,
|
sortIndex: existing.sortIndex,
|
||||||
|
uiId: existing.uiId,
|
||||||
config: parsedConfig,
|
config: parsedConfig,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.promptVariant.update({
|
// Hide anything with the same uiId besides the new one
|
||||||
|
await prisma.promptVariant.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
uiId: existing.uiId,
|
||||||
|
id: {
|
||||||
|
not: newVariant.id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
visible: false,
|
visible: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user