diff --git a/js/app/src/Routes.tsx b/js/app/src/Routes.tsx index 73893119464..405e0c4d432 100644 --- a/js/app/src/Routes.tsx +++ b/js/app/src/Routes.tsx @@ -172,6 +172,23 @@ export const revalidateOnProjectChange: ShouldRevalidateFunction = ({ return currentParams.projectId !== nextParams.projectId; }; +// :promptId's loader always revalidates so versions saved elsewhere show up, +// except when opening or switching versions of the same prompt: its query +// holds only the first page of the paginated versions list, so re-running it +// would drop the pages already scrolled in, including the version just clicked. +const revalidatePromptUnlessOpeningVersion: ShouldRevalidateFunction = ({ + currentUrl, + nextUrl, + currentParams, + nextParams, +}) => { + if (currentUrl.href === nextUrl.href) return true; + const isOpeningVersion = + currentParams.promptId === nextParams.promptId && + nextParams.versionId != null; + return !isOpeningVersion; +}; + // Shared by the traces and spans tabs, which both host the compare drawer const compareTracesRouteHandle = { agentRoute: { @@ -844,9 +861,7 @@ export const appRouteObjects = createRoutesFromElements( true} + shouldRevalidate={revalidatePromptUnlessOpeningVersion} handle={{ crumb: (data: PromptLoaderData) => { if (data?.prompt?.__typename === "Prompt") { diff --git a/js/app/src/components/dataset/__generated__/CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation.graphql.ts b/js/app/src/components/dataset/__generated__/CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation.graphql.ts index 72fffe97a47..bfea1e04dc0 100644 --- a/js/app/src/components/dataset/__generated__/CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation.graphql.ts +++ b/js/app/src/components/dataset/__generated__/CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<6f0bc573b6738f0e31d18216e2ef289c>> * @lightSyntaxTransform */ @@ -505,7 +505,14 @@ v24 = { "kind": "ScalarField", "name": "backendType", "storageKey": null -}; +}, +v25 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } +]; return { "fragment": { "argumentDefinitions": [ @@ -786,8 +793,8 @@ return { "plural": false, "selections": [ { - "alias": null, - "args": null, + "alias": "pagedVersions", + "args": (v25/*:: as any*/), "concreteType": "PromptVersionConnection", "kind": "LinkedField", "name": "promptVersions", @@ -845,12 +852,66 @@ return { } ], "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PromptVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v8/*:: as any*/), + (v3/*:: as any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null } ], "storageKey": null } ], - "storageKey": null + "storageKey": "promptVersions(first:50)" + }, + { + "alias": "pagedVersions", + "args": (v25/*:: as any*/), + "filters": null, + "handle": "connection", + "key": "PromptVersionsList__pagedVersions", + "kind": "LinkedHandle", + "name": "promptVersions" }, (v3/*:: as any*/), (v4/*:: as any*/) @@ -997,12 +1058,12 @@ return { ] }, "params": { - "cacheID": "859a799f23c5e3f94b471245396853f8", + "cacheID": "eafc938d029480b9774e5737939db360", "id": null, "metadata": {}, "name": "CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation", "operationKind": "mutation", - "text": "mutation CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation(\n $input: CreateDatasetLLMEvaluatorInput!\n) {\n createDatasetLlmEvaluator(input: $input) {\n evaluator {\n id\n name\n ...PlaygroundDatasetSection_evaluator\n evaluator {\n __typename\n ... on LLMEvaluator {\n prompt {\n ...PromptVersionsList__main\n id\n }\n }\n id\n }\n ...DatasetEvaluatorsTable_row\n }\n }\n}\n\nfragment DatasetEvaluatorsTable_row on DatasetEvaluator {\n id\n name\n description\n updatedAt\n user {\n username\n profilePictureUrl\n id\n }\n evaluator {\n __typename\n id\n name\n kind\n createdAt\n updatedAt\n ... on LLMEvaluator {\n prompt {\n id\n name\n }\n promptVersionTag {\n name\n id\n }\n promptVersion {\n modelName\n modelProvider\n id\n }\n }\n ... on CodeEvaluator {\n language\n sandboxConfig {\n id\n name\n provider {\n backendType\n id\n }\n }\n }\n }\n}\n\nfragment PlaygroundDatasetSection_evaluator on DatasetEvaluator {\n id\n name\n inputMapping {\n literalMapping\n pathMapping\n }\n evaluator {\n __typename\n id\n kind\n isBuiltin\n }\n outputConfigs {\n __typename\n ... on CategoricalAnnotationConfig {\n name\n optimizationDirection\n values {\n label\n score\n }\n }\n ... on ContinuousAnnotationConfig {\n name\n optimizationDirection\n lowerBound\n upperBound\n }\n ... on FreeformAnnotationConfig {\n name\n optimizationDirection\n threshold\n lowerBound\n upperBound\n }\n ... on Node {\n __isNode: __typename\n id\n }\n }\n}\n\nfragment PromptVersionSummaryFragment on PromptVersion {\n id\n description\n sequenceNumber\n createdAt\n user {\n id\n username\n profilePictureUrl\n }\n ...PromptVersionTagsList_data\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n\nfragment PromptVersionsList__main on Prompt {\n promptVersions {\n edges {\n version: node {\n id\n ...PromptVersionSummaryFragment\n }\n }\n }\n}\n" + "text": "mutation CreateLLMDatasetEvaluatorSlideover_createLLMEvaluatorMutation(\n $input: CreateDatasetLLMEvaluatorInput!\n) {\n createDatasetLlmEvaluator(input: $input) {\n evaluator {\n id\n name\n ...PlaygroundDatasetSection_evaluator\n evaluator {\n __typename\n ... on LLMEvaluator {\n prompt {\n ...PromptVersionsList__main\n id\n }\n }\n id\n }\n ...DatasetEvaluatorsTable_row\n }\n }\n}\n\nfragment DatasetEvaluatorsTable_row on DatasetEvaluator {\n id\n name\n description\n updatedAt\n user {\n username\n profilePictureUrl\n id\n }\n evaluator {\n __typename\n id\n name\n kind\n createdAt\n updatedAt\n ... on LLMEvaluator {\n prompt {\n id\n name\n }\n promptVersionTag {\n name\n id\n }\n promptVersion {\n modelName\n modelProvider\n id\n }\n }\n ... on CodeEvaluator {\n language\n sandboxConfig {\n id\n name\n provider {\n backendType\n id\n }\n }\n }\n }\n}\n\nfragment PlaygroundDatasetSection_evaluator on DatasetEvaluator {\n id\n name\n inputMapping {\n literalMapping\n pathMapping\n }\n evaluator {\n __typename\n id\n kind\n isBuiltin\n }\n outputConfigs {\n __typename\n ... on CategoricalAnnotationConfig {\n name\n optimizationDirection\n values {\n label\n score\n }\n }\n ... on ContinuousAnnotationConfig {\n name\n optimizationDirection\n lowerBound\n upperBound\n }\n ... on FreeformAnnotationConfig {\n name\n optimizationDirection\n threshold\n lowerBound\n upperBound\n }\n ... on Node {\n __isNode: __typename\n id\n }\n }\n}\n\nfragment PromptVersionSummaryFragment on PromptVersion {\n id\n description\n sequenceNumber\n createdAt\n user {\n id\n username\n profilePictureUrl\n }\n ...PromptVersionTagsList_data\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n\nfragment PromptVersionsList__main on Prompt {\n pagedVersions: promptVersions(first: 50) {\n edges {\n version: node {\n id\n ...PromptVersionSummaryFragment\n }\n cursor\n node {\n __typename\n id\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n" } }; })(); diff --git a/js/app/src/components/prompt/TagPromptVersionButton.tsx b/js/app/src/components/prompt/TagPromptVersionButton.tsx index 3dfbde382b7..4df6ea5e31d 100644 --- a/js/app/src/components/prompt/TagPromptVersionButton.tsx +++ b/js/app/src/components/prompt/TagPromptVersionButton.tsx @@ -14,11 +14,14 @@ import { PopoverArrow, View, } from "@phoenix/components"; +import type { TagPromptVersionButtonSetTagMutation } from "@phoenix/components/prompt/__generated__/TagPromptVersionButtonSetTagMutation.graphql"; import type { TagPromptVersionButtonTagsQuery } from "@phoenix/components/prompt/__generated__/TagPromptVersionButtonTagsQuery.graphql"; import { DEFAULT_PROMPT_VERSION_TAGS } from "@phoenix/constants"; import { useNotifySuccess } from "@phoenix/contexts"; import { NewPromptVersionDialog } from "@phoenix/pages/prompt/NewPromptVersionTagDialog"; +import { movePromptVersionTag } from "./movePromptVersionTag"; + type TagPromptVersionButtonProps = { promptId: string; versionId: string; @@ -93,6 +96,7 @@ export function TagPromptVersionButton({ }} > setShowNewTagDialog(false)} onNewTagCreated={(newTag) => { @@ -163,22 +167,20 @@ function TagList({ ); }, [allVersionTags]); - const [commitSetTag, isCommitting] = useMutation(graphql` - mutation TagPromptVersionButtonSetTagMutation( - $input: SetPromptVersionTagInput! - $promptId: ID! - ) { - setPromptVersionTag(input: $input) { - query { - prompt: node(id: $promptId) { - ... on Prompt { - ...PromptVersionsList__main - } + const [commitSetTag, isCommitting] = + useMutation(graphql` + mutation TagPromptVersionButtonSetTagMutation( + $input: SetPromptVersionTagInput! + ) { + setPromptVersionTag(input: $input) { + promptVersionTag { + id + name + promptVersionId } } } - } - `); + `); return (
    {allVersionTagsWithDefaults.map((tagName: string) => { @@ -213,7 +215,18 @@ function TagList({ promptVersionId: versionId, description, }, - promptId: promptId, + }, + updater: (store, data) => { + const tagId = + data?.setPromptVersionTag.promptVersionTag?.id; + if (tagId) { + movePromptVersionTag({ + store, + promptId, + versionId, + tagId, + }); + } }, onCompleted: () => { onTagSet(tagName); diff --git a/js/app/src/components/prompt/__generated__/TagPromptVersionButtonSetTagMutation.graphql.ts b/js/app/src/components/prompt/__generated__/TagPromptVersionButtonSetTagMutation.graphql.ts index 731e38b4e0e..08331d7689a 100644 --- a/js/app/src/components/prompt/__generated__/TagPromptVersionButtonSetTagMutation.graphql.ts +++ b/js/app/src/components/prompt/__generated__/TagPromptVersionButtonSetTagMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<3c2b0602a136cb7b27b0179ad1e170e4>> * @lightSyntaxTransform */ @@ -8,7 +8,6 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; export type SetPromptVersionTagInput = { description?: string | null; name: string; @@ -16,15 +15,14 @@ export type SetPromptVersionTagInput = { }; export type TagPromptVersionButtonSetTagMutation$variables = { input: SetPromptVersionTagInput; - promptId: string; }; export type TagPromptVersionButtonSetTagMutation$data = { readonly setPromptVersionTag: { - readonly query: { - readonly prompt: { - readonly " $fragmentSpreads": FragmentRefs<"PromptVersionsList__main">; - }; - }; + readonly promptVersionTag: { + readonly id: string; + readonly name: string; + readonly promptVersionId: string; + } | null; }; }; export type TagPromptVersionButtonSetTagMutation = { @@ -38,87 +36,66 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "input" - }, - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "promptId" } ], v1 = [ { - "kind": "Variable", - "name": "input", - "variableName": "input" - } -], -v2 = [ - { - "kind": "Variable", - "name": "id", - "variableName": "promptId" - } -], -v3 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}; -return { - "fragment": { - "argumentDefinitions": (v0/*:: as any*/), - "kind": "Fragment", - "metadata": null, - "name": "TagPromptVersionButtonSetTagMutation", + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "PromptVersionTagMutationPayload", + "kind": "LinkedField", + "name": "setPromptVersionTag", + "plural": false, "selections": [ { "alias": null, - "args": (v1/*:: as any*/), - "concreteType": "PromptVersionTagMutationPayload", + "args": null, + "concreteType": "PromptVersionTag", "kind": "LinkedField", - "name": "setPromptVersionTag", + "name": "promptVersionTag", "plural": false, "selections": [ { "alias": null, "args": null, - "concreteType": "Query", - "kind": "LinkedField", - "name": "query", - "plural": false, - "selections": [ - { - "alias": "prompt", - "args": (v2/*:: as any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "kind": "InlineFragment", - "selections": [ - { - "args": null, - "kind": "FragmentSpread", - "name": "PromptVersionsList__main" - } - ], - "type": "Prompt", - "abstractKey": null - } - ], - "storageKey": null - } - ], + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "promptVersionId", "storageKey": null } ], "storageKey": null } ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*:: as any*/), + "kind": "Fragment", + "metadata": null, + "name": "TagPromptVersionButtonSetTagMutation", + "selections": (v1/*:: as any*/), "type": "Mutation", "abstractKey": null }, @@ -127,168 +104,19 @@ return { "argumentDefinitions": (v0/*:: as any*/), "kind": "Operation", "name": "TagPromptVersionButtonSetTagMutation", - "selections": [ - { - "alias": null, - "args": (v1/*:: as any*/), - "concreteType": "PromptVersionTagMutationPayload", - "kind": "LinkedField", - "name": "setPromptVersionTag", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Query", - "kind": "LinkedField", - "name": "query", - "plural": false, - "selections": [ - { - "alias": "prompt", - "args": (v2/*:: as any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, - { - "kind": "InlineFragment", - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PromptVersionConnection", - "kind": "LinkedField", - "name": "promptVersions", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PromptVersionEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": "version", - "args": null, - "concreteType": "PromptVersion", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v3/*:: as any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "description", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "sequenceNumber", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "User", - "kind": "LinkedField", - "name": "user", - "plural": false, - "selections": [ - (v3/*:: as any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "username", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "profilePictureUrl", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "PromptVersionTag", - "kind": "LinkedField", - "name": "tags", - "plural": true, - "selections": [ - (v3/*:: as any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "type": "Prompt", - "abstractKey": null - }, - (v3/*:: as any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ] + "selections": (v1/*:: as any*/) }, "params": { - "cacheID": "991a6a38de992c1261cceafe9b2c6850", + "cacheID": "5c5d28c661d4d918f165516095b7a487", "id": null, "metadata": {}, "name": "TagPromptVersionButtonSetTagMutation", "operationKind": "mutation", - "text": "mutation TagPromptVersionButtonSetTagMutation(\n $input: SetPromptVersionTagInput!\n $promptId: ID!\n) {\n setPromptVersionTag(input: $input) {\n query {\n prompt: node(id: $promptId) {\n __typename\n ... on Prompt {\n ...PromptVersionsList__main\n }\n id\n }\n }\n }\n}\n\nfragment PromptVersionSummaryFragment on PromptVersion {\n id\n description\n sequenceNumber\n createdAt\n user {\n id\n username\n profilePictureUrl\n }\n ...PromptVersionTagsList_data\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n\nfragment PromptVersionsList__main on Prompt {\n promptVersions {\n edges {\n version: node {\n id\n ...PromptVersionSummaryFragment\n }\n }\n }\n}\n" + "text": "mutation TagPromptVersionButtonSetTagMutation(\n $input: SetPromptVersionTagInput!\n) {\n setPromptVersionTag(input: $input) {\n promptVersionTag {\n id\n name\n promptVersionId\n }\n }\n}\n" } }; })(); -(node as any).hash = "e6cca3095cc459928e5d7ff1e7137a43"; +(node as any).hash = "fac68f8d5a8c9cd286ee972ac9543d39"; export default node; diff --git a/js/app/src/components/prompt/__tests__/movePromptVersionTag.test.ts b/js/app/src/components/prompt/__tests__/movePromptVersionTag.test.ts new file mode 100644 index 00000000000..1a2fbcb8853 --- /dev/null +++ b/js/app/src/components/prompt/__tests__/movePromptVersionTag.test.ts @@ -0,0 +1,140 @@ +import { commitLocalUpdate } from "react-relay"; +import { + createOperationDescriptor, + Environment, + Network, + RecordSource, + Store, +} from "relay-runtime"; +import { describe, expect, it } from "vitest"; + +import PromptVersionsListPaginationQuery from "@phoenix/pages/prompt/__generated__/PromptVersionsListPaginationQuery.graphql"; + +import { movePromptVersionTag } from "../movePromptVersionTag"; + +const PROMPT_ID = "prompt-1"; +const PRODUCTION_TAG = { id: "tag-production", name: "production" }; +const STAGING_TAG = { id: "tag-staging", name: "staging" }; + +function versionEdge({ + sequenceNumber, + tags, +}: { + sequenceNumber: number; + tags: (typeof PRODUCTION_TAG)[]; +}) { + const version = { + __typename: "PromptVersion", + id: `version-${sequenceNumber}`, + description: null, + sequenceNumber, + createdAt: "2026-01-01T00:00:00Z", + user: null, + tags, + }; + return { cursor: `cursor-${sequenceNumber}`, version, node: version }; +} + +/** + * Loads two pages of the versions list into the store the way + * `PromptVersionsList` does, so the second page is only reachable through the + * connection: version 3 holds "production", version 1 holds "staging". + */ +function createEnvironmentWithTwoLoadedPages() { + const environment = new Environment({ + network: Network.create(() => { + throw new Error("the store update must not fetch"); + }), + store: new Store(new RecordSource()), + }); + const pages = [ + { + after: null, + edges: [ + versionEdge({ sequenceNumber: 3, tags: [PRODUCTION_TAG] }), + versionEdge({ sequenceNumber: 2, tags: [] }), + ], + hasNextPage: true, + }, + { + after: "cursor-2", + edges: [versionEdge({ sequenceNumber: 1, tags: [STAGING_TAG] })], + hasNextPage: false, + }, + ]; + for (const { after, edges, hasNextPage } of pages) { + environment.commitPayload( + createOperationDescriptor(PromptVersionsListPaginationQuery, { + id: PROMPT_ID, + first: 2, + after, + }), + { + node: { + __typename: "Prompt", + id: PROMPT_ID, + pagedVersions: { + edges, + pageInfo: { + endCursor: edges[edges.length - 1].cursor, + hasNextPage, + }, + }, + }, + } + ); + } + return environment; +} + +function getTagNamesByVersionId(environment: Environment) { + const tagNamesByVersionId: Record = {}; + commitLocalUpdate(environment, (store) => { + for (const versionId of ["version-3", "version-2", "version-1"]) { + tagNamesByVersionId[versionId] = ( + store.get(versionId)?.getLinkedRecords("tags") ?? [] + ).map((tag) => String(tag?.getValue("name"))); + } + }); + return tagNamesByVersionId; +} + +describe("movePromptVersionTag", () => { + it("moves the tag onto a version on a later page and off its previous version", () => { + const environment = createEnvironmentWithTwoLoadedPages(); + + commitLocalUpdate(environment, (store) => + movePromptVersionTag({ + store, + promptId: PROMPT_ID, + versionId: "version-1", + tagId: PRODUCTION_TAG.id, + }) + ); + + expect(getTagNamesByVersionId(environment)).toEqual({ + "version-3": [], + "version-2": [], + "version-1": ["staging", "production"], + }); + }); + + it("does not duplicate a tag the version already holds", () => { + const environment = createEnvironmentWithTwoLoadedPages(); + + commitLocalUpdate(environment, (store) => + movePromptVersionTag({ + store, + promptId: PROMPT_ID, + versionId: "version-3", + tagId: PRODUCTION_TAG.id, + }) + ); + + expect(getTagNamesByVersionId(environment)).toEqual({ + "version-3": ["production"], + "version-2": [], + "version-1": ["staging"], + }); + }); +}); diff --git a/js/app/src/components/prompt/movePromptVersionTag.ts b/js/app/src/components/prompt/movePromptVersionTag.ts new file mode 100644 index 00000000000..f27b2d17ddd --- /dev/null +++ b/js/app/src/components/prompt/movePromptVersionTag.ts @@ -0,0 +1,43 @@ +import type { RecordProxy, RecordSourceProxy } from "relay-runtime"; +import { ConnectionHandler } from "relay-runtime"; + +/** + * Store update for `setPromptVersionTag`. A tag belongs to one version at a + * time, so it is added to the tagged version and removed from every other + * version loaded in the prompt's paginated versions list. + */ +export function movePromptVersionTag({ + store, + promptId, + versionId, + tagId, +}: { + store: RecordSourceProxy; + promptId: string; + versionId: string; + tagId: string; +}) { + const tag = store.get(tagId); + const prompt = store.get(promptId); + if (!tag || !prompt) { + return; + } + const loadedVersions = + ConnectionHandler.getConnection(prompt, "PromptVersionsList__pagedVersions") + ?.getLinkedRecords("edges") + ?.map((edge) => edge?.getLinkedRecord("node")) ?? []; + [...loadedVersions, store.get(versionId)].forEach((version) => { + const versionTags = version?.getLinkedRecords("tags"); + if (!version || !versionTags) { + return; + } + const otherTags = versionTags.filter( + (versionTag): versionTag is RecordProxy => + versionTag != null && versionTag.getDataID() !== tagId + ); + version.setLinkedRecords( + version.getDataID() === versionId ? [...otherTags, tag] : otherTags, + "tags" + ); + }); +} diff --git a/js/app/src/pages/prompt/NewPromptVersionTagDialog.tsx b/js/app/src/pages/prompt/NewPromptVersionTagDialog.tsx index 38f7921f948..a746839306f 100644 --- a/js/app/src/pages/prompt/NewPromptVersionTagDialog.tsx +++ b/js/app/src/pages/prompt/NewPromptVersionTagDialog.tsx @@ -23,8 +23,10 @@ import { TextField, View, } from "@phoenix/components"; +import { movePromptVersionTag } from "@phoenix/components/prompt/movePromptVersionTag"; import { validateIdentifier } from "../../utils/identifierUtils"; +import type { NewPromptVersionTagDialogMutation } from "./__generated__/NewPromptVersionTagDialogMutation.graphql"; type PromptVersionTagFormParams = { name: string; @@ -33,9 +35,14 @@ type PromptVersionTagFormParams = { export function NewPromptVersionDialog({ onNewTagCreated, + promptId, promptVersionId, onDismiss, }: { + /** + * The prompt the version belongs to + */ + promptId: string; /** * The prompt version to set the tag on */ @@ -55,20 +62,20 @@ export function NewPromptVersionDialog({ mode: "onChange", }); const [error, setError] = useState(null); - const [commitCreate, isCommitting] = useMutation(graphql` - mutation NewPromptVersionTagDialogMutation( - $input: SetPromptVersionTagInput! - $promptVersionId: ID! - ) { - setPromptVersionTag(input: $input) { - query { - node(id: $promptVersionId) { - ...PromptVersionTagsList_data + const [commitCreate, isCommitting] = + useMutation(graphql` + mutation NewPromptVersionTagDialogMutation( + $input: SetPromptVersionTagInput! + ) { + setPromptVersionTag(input: $input) { + promptVersionTag { + id + name + promptVersionId } } } - } - `); + `); const onSubmit = (newTag: PromptVersionTagFormParams) => { setError(null); commitCreate({ @@ -78,7 +85,17 @@ export function NewPromptVersionDialog({ description: newTag.description, promptVersionId: promptVersionId, }, - promptVersionId, + }, + updater: (store, data) => { + const tagId = data?.setPromptVersionTag.promptVersionTag?.id; + if (tagId) { + movePromptVersionTag({ + store, + promptId, + versionId: promptVersionId, + tagId, + }); + } }, onCompleted: () => { onNewTagCreated(newTag); diff --git a/js/app/src/pages/prompt/PromptLayout.tsx b/js/app/src/pages/prompt/PromptLayout.tsx index d5528e7e19a..d041cf8f49c 100644 --- a/js/app/src/pages/prompt/PromptLayout.tsx +++ b/js/app/src/pages/prompt/PromptLayout.tsx @@ -83,13 +83,7 @@ export function PromptLayout() { id name } - promptVersions { - edges { - node { - id - } - } - } + versionCount } `, preloadedData.prompt @@ -158,7 +152,7 @@ export function PromptLayout() { Prompt - Versions {data.promptVersions.edges.length} + Versions {data.versionCount} Config diff --git a/js/app/src/pages/prompt/PromptVersionDetailsPage.tsx b/js/app/src/pages/prompt/PromptVersionDetailsPage.tsx index 5de8d0b1685..e4e9ed770d8 100644 --- a/js/app/src/pages/prompt/PromptVersionDetailsPage.tsx +++ b/js/app/src/pages/prompt/PromptVersionDetailsPage.tsx @@ -94,7 +94,7 @@ function PromptVersionDetailsPageContent({ { - const { promptVersions } = useFragment( + const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment( graphql` - fragment PromptVersionsList__main on Prompt { - promptVersions { + fragment PromptVersionsList__main on Prompt + @refetchable(queryName: "PromptVersionsListPaginationQuery") + @argumentDefinitions( + after: { type: "String", defaultValue: null } + first: { type: "Int", defaultValue: 50 } + ) { + pagedVersions: promptVersions(first: $first, after: $after) + @connection(key: "PromptVersionsList__pagedVersions") { edges { version: node { id @@ -85,24 +98,38 @@ export const PromptVersionsList = ({ `, prompt ); + const fetchMoreOnBottomReached = (scrollContainer: HTMLDivElement) => { + const { scrollHeight, scrollTop, clientHeight } = scrollContainer; + if ( + scrollHeight - scrollTop - clientHeight < 300 && + !isLoadingNext && + hasNext + ) { + loadNext(PAGE_SIZE); + } + }; return ( - - {promptVersions.edges.map(({ version }) => ( - - ))} - +
    fetchMoreOnBottomReached(event.currentTarget)} + > + + {data.pagedVersions.edges.map(({ version }) => ( + + ))} + +
    ); }; diff --git a/js/app/src/pages/prompt/__generated__/NewPromptVersionTagDialogMutation.graphql.ts b/js/app/src/pages/prompt/__generated__/NewPromptVersionTagDialogMutation.graphql.ts index 3e50b01ee82..5501a2ce39f 100644 --- a/js/app/src/pages/prompt/__generated__/NewPromptVersionTagDialogMutation.graphql.ts +++ b/js/app/src/pages/prompt/__generated__/NewPromptVersionTagDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<81a847bbae04fd5c75c837b60ea2e79b>> + * @generated SignedSource<> * @lightSyntaxTransform */ @@ -8,7 +8,6 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; export type SetPromptVersionTagInput = { description?: string | null; name: string; @@ -16,15 +15,14 @@ export type SetPromptVersionTagInput = { }; export type NewPromptVersionTagDialogMutation$variables = { input: SetPromptVersionTagInput; - promptVersionId: string; }; export type NewPromptVersionTagDialogMutation$data = { readonly setPromptVersionTag: { - readonly query: { - readonly node: { - readonly " $fragmentSpreads": FragmentRefs<"PromptVersionTagsList_data">; - }; - }; + readonly promptVersionTag: { + readonly id: string; + readonly name: string; + readonly promptVersionId: string; + } | null; }; }; export type NewPromptVersionTagDialogMutation = { @@ -38,80 +36,66 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "input" - }, - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "promptVersionId" } ], v1 = [ { - "kind": "Variable", - "name": "input", - "variableName": "input" - } -], -v2 = [ - { - "kind": "Variable", - "name": "id", - "variableName": "promptVersionId" - } -], -v3 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}; -return { - "fragment": { - "argumentDefinitions": (v0/*:: as any*/), - "kind": "Fragment", - "metadata": null, - "name": "NewPromptVersionTagDialogMutation", + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "PromptVersionTagMutationPayload", + "kind": "LinkedField", + "name": "setPromptVersionTag", + "plural": false, "selections": [ { "alias": null, - "args": (v1/*:: as any*/), - "concreteType": "PromptVersionTagMutationPayload", + "args": null, + "concreteType": "PromptVersionTag", "kind": "LinkedField", - "name": "setPromptVersionTag", + "name": "promptVersionTag", "plural": false, "selections": [ { "alias": null, "args": null, - "concreteType": "Query", - "kind": "LinkedField", - "name": "query", - "plural": false, - "selections": [ - { - "alias": null, - "args": (v2/*:: as any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "args": null, - "kind": "FragmentSpread", - "name": "PromptVersionTagsList_data" - } - ], - "storageKey": null - } - ], + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "promptVersionId", "storageKey": null } ], "storageKey": null } ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*:: as any*/), + "kind": "Fragment", + "metadata": null, + "name": "NewPromptVersionTagDialogMutation", + "selections": (v1/*:: as any*/), "type": "Mutation", "abstractKey": null }, @@ -120,87 +104,19 @@ return { "argumentDefinitions": (v0/*:: as any*/), "kind": "Operation", "name": "NewPromptVersionTagDialogMutation", - "selections": [ - { - "alias": null, - "args": (v1/*:: as any*/), - "concreteType": "PromptVersionTagMutationPayload", - "kind": "LinkedField", - "name": "setPromptVersionTag", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Query", - "kind": "LinkedField", - "name": "query", - "plural": false, - "selections": [ - { - "alias": null, - "args": (v2/*:: as any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, - { - "kind": "InlineFragment", - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PromptVersionTag", - "kind": "LinkedField", - "name": "tags", - "plural": true, - "selections": [ - (v3/*:: as any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - } - ], - "storageKey": null - } - ], - "type": "PromptVersion", - "abstractKey": null - }, - (v3/*:: as any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ] + "selections": (v1/*:: as any*/) }, "params": { - "cacheID": "bfc7b301944fc8273b56810aa777401c", + "cacheID": "df5ac258295bdce47280b043147f5e76", "id": null, "metadata": {}, "name": "NewPromptVersionTagDialogMutation", "operationKind": "mutation", - "text": "mutation NewPromptVersionTagDialogMutation(\n $input: SetPromptVersionTagInput!\n $promptVersionId: ID!\n) {\n setPromptVersionTag(input: $input) {\n query {\n node(id: $promptVersionId) {\n __typename\n ...PromptVersionTagsList_data\n id\n }\n }\n }\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n" + "text": "mutation NewPromptVersionTagDialogMutation(\n $input: SetPromptVersionTagInput!\n) {\n setPromptVersionTag(input: $input) {\n promptVersionTag {\n id\n name\n promptVersionId\n }\n }\n}\n" } }; })(); -(node as any).hash = "a6e4352854be2ab5fd0c10368a3469ad"; +(node as any).hash = "477e6cb4227b4970f2f109fae4502d00"; export default node; diff --git a/js/app/src/pages/prompt/__generated__/PromptLayout__main.graphql.ts b/js/app/src/pages/prompt/__generated__/PromptLayout__main.graphql.ts index 32922aea784..ef223e8267b 100644 --- a/js/app/src/pages/prompt/__generated__/PromptLayout__main.graphql.ts +++ b/js/app/src/pages/prompt/__generated__/PromptLayout__main.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<0bf28c13b37556e87942073adbf6aad7>> + * @generated SignedSource<<6e33dbfd63bd87bc97a0b61bcdbd201f>> * @lightSyntaxTransform */ @@ -14,17 +14,11 @@ export type PromptLayout__main$data = { readonly id: string; readonly metadata: any; readonly name: string; - readonly promptVersions: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly id: string; - }; - }>; - }; readonly sourcePrompt: { readonly id: string; readonly name: string; } | null; + readonly versionCount: number; readonly " $fragmentType": "PromptLayout__main"; }; export type PromptLayout__main$key = { @@ -85,35 +79,8 @@ return { { "alias": null, "args": null, - "concreteType": "PromptVersionConnection", - "kind": "LinkedField", - "name": "promptVersions", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PromptVersionEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PromptVersion", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v0/*:: as any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], + "kind": "ScalarField", + "name": "versionCount", "storageKey": null } ], @@ -122,6 +89,6 @@ return { }; })(); -(node as any).hash = "1778112f7fe3cd7c75472a1889cce0d9"; +(node as any).hash = "c6fc27d4f5202b24b3f1c42cdf893f25"; export default node; diff --git a/js/app/src/pages/prompt/__generated__/PromptVersionsListPaginationQuery.graphql.ts b/js/app/src/pages/prompt/__generated__/PromptVersionsListPaginationQuery.graphql.ts new file mode 100644 index 00000000000..653b38d9587 --- /dev/null +++ b/js/app/src/pages/prompt/__generated__/PromptVersionsListPaginationQuery.graphql.ts @@ -0,0 +1,300 @@ +/** + * @generated SignedSource<<6b5b2611690dcb7af24f2a4491560f0c>> + * @lightSyntaxTransform + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type PromptVersionsListPaginationQuery$variables = { + after?: string | null; + first?: number | null; + id: string; +}; +export type PromptVersionsListPaginationQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"PromptVersionsList__main">; + }; +}; +export type PromptVersionsListPaginationQuery = { + response: PromptVersionsListPaginationQuery$data; + variables: PromptVersionsListPaginationQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": 50, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v2 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "after" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "first" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*:: as any*/), + "kind": "Fragment", + "metadata": null, + "name": "PromptVersionsListPaginationQuery", + "selections": [ + { + "alias": null, + "args": (v1/*:: as any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": (v2/*:: as any*/), + "kind": "FragmentSpread", + "name": "PromptVersionsList__main" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*:: as any*/), + "kind": "Operation", + "name": "PromptVersionsListPaginationQuery", + "selections": [ + { + "alias": null, + "args": (v1/*:: as any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*:: as any*/), + (v4/*:: as any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": "pagedVersions", + "args": (v2/*:: as any*/), + "concreteType": "PromptVersionConnection", + "kind": "LinkedField", + "name": "promptVersions", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PromptVersionEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": "version", + "args": null, + "concreteType": "PromptVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v4/*:: as any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sequenceNumber", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "user", + "plural": false, + "selections": [ + (v4/*:: as any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "username", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "profilePictureUrl", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PromptVersionTag", + "kind": "LinkedField", + "name": "tags", + "plural": true, + "selections": [ + (v4/*:: as any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PromptVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*:: as any*/), + (v4/*:: as any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": "pagedVersions", + "args": (v2/*:: as any*/), + "filters": null, + "handle": "connection", + "key": "PromptVersionsList__pagedVersions", + "kind": "LinkedHandle", + "name": "promptVersions" + } + ], + "type": "Prompt", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "a663c557ca430beeec2031f78fc4b5df", + "id": null, + "metadata": {}, + "name": "PromptVersionsListPaginationQuery", + "operationKind": "query", + "text": "query PromptVersionsListPaginationQuery(\n $after: String = null\n $first: Int = 50\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PromptVersionsList__main_2HEEH6\n id\n }\n}\n\nfragment PromptVersionSummaryFragment on PromptVersion {\n id\n description\n sequenceNumber\n createdAt\n user {\n id\n username\n profilePictureUrl\n }\n ...PromptVersionTagsList_data\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n\nfragment PromptVersionsList__main_2HEEH6 on Prompt {\n pagedVersions: promptVersions(first: $first, after: $after) {\n edges {\n version: node {\n id\n ...PromptVersionSummaryFragment\n }\n cursor\n node {\n __typename\n id\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "caccb1017f626d715a22974e2aefd2aa"; + +export default node; diff --git a/js/app/src/pages/prompt/__generated__/PromptVersionsList__main.graphql.ts b/js/app/src/pages/prompt/__generated__/PromptVersionsList__main.graphql.ts index cb767acc222..91ebbd28004 100644 --- a/js/app/src/pages/prompt/__generated__/PromptVersionsList__main.graphql.ts +++ b/js/app/src/pages/prompt/__generated__/PromptVersionsList__main.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<8cba8cfbeae698734d8e9b3bae373770>> * @lightSyntaxTransform */ @@ -10,7 +10,8 @@ import { ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type PromptVersionsList__main$data = { - readonly promptVersions: { + readonly id: string; + readonly pagedVersions: { readonly edges: ReadonlyArray<{ readonly version: { readonly id: string; @@ -25,18 +26,69 @@ export type PromptVersionsList__main$key = { readonly " $fragmentSpreads": FragmentRefs<"PromptVersionsList__main">; }; -const node: ReaderFragment = { - "argumentDefinitions": [], +import PromptVersionsListPaginationQuery_graphql from './PromptVersionsListPaginationQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "pagedVersions" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": 50, + "kind": "LocalArgument", + "name": "first" + } + ], "kind": "Fragment", - "metadata": null, + "metadata": { + "connection": [ + { + "count": "first", + "cursor": "after", + "direction": "forward", + "path": (v0/*:: as any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": null, + "path": (v0/*:: as any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": PromptVersionsListPaginationQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, "name": "PromptVersionsList__main", "selections": [ { - "alias": null, + "alias": "pagedVersions", "args": null, "concreteType": "PromptVersionConnection", "kind": "LinkedField", - "name": "promptVersions", + "name": "__PromptVersionsList__pagedVersions_connection", "plural": false, "selections": [ { @@ -54,33 +106,79 @@ const node: ReaderFragment = { "kind": "LinkedField", "name": "node", "plural": false, + "selections": [ + (v1/*:: as any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "PromptVersionSummaryFragment" + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PromptVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, "selections": [ { "alias": null, "args": null, "kind": "ScalarField", - "name": "id", + "name": "__typename", "storageKey": null - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "PromptVersionSummaryFragment" } ], "storageKey": null } ], "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null } ], "storageKey": null - } + }, + (v1/*:: as any*/) ], "type": "Prompt", "abstractKey": null }; +})(); -(node as any).hash = "30cac642c3466fa8405eb71f29d27be3"; +(node as any).hash = "caccb1017f626d715a22974e2aefd2aa"; export default node; diff --git a/js/app/src/pages/prompt/__generated__/promptLoaderQuery.graphql.ts b/js/app/src/pages/prompt/__generated__/promptLoaderQuery.graphql.ts index da9cde6a4b9..c3ab49cbc50 100644 --- a/js/app/src/pages/prompt/__generated__/promptLoaderQuery.graphql.ts +++ b/js/app/src/pages/prompt/__generated__/promptLoaderQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform */ @@ -128,14 +128,14 @@ v14 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "sequenceNumber", + "name": "createdAt", "storageKey": null }, v15 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "createdAt", + "name": "sequenceNumber", "storageKey": null }, v16 = { @@ -177,7 +177,14 @@ v18 = { "plural": true, "selections": (v17/*:: as any*/), "storageKey": null -}; +}, +v19 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } +]; return { "fragment": { "argumentDefinitions": (v0/*:: as any*/), @@ -830,23 +837,6 @@ return { } ], "storageKey": null - }, - { - "alias": "version", - "args": null, - "concreteType": "PromptVersion", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v3/*:: as any*/), - (v11/*:: as any*/), - (v14/*:: as any*/), - (v15/*:: as any*/), - (v16/*:: as any*/), - (v18/*:: as any*/) - ], - "storageKey": null } ], "storageKey": null @@ -894,8 +884,8 @@ return { "selections": [ (v3/*:: as any*/), (v11/*:: as any*/), - (v15/*:: as any*/), (v14/*:: as any*/), + (v15/*:: as any*/), (v16/*:: as any*/), (v18/*:: as any*/) ], @@ -927,6 +917,99 @@ return { ], "storageKey": null }, + { + "alias": "pagedVersions", + "args": (v19/*:: as any*/), + "concreteType": "PromptVersionConnection", + "kind": "LinkedField", + "name": "promptVersions", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PromptVersionEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": "version", + "args": null, + "concreteType": "PromptVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*:: as any*/), + (v11/*:: as any*/), + (v15/*:: as any*/), + (v14/*:: as any*/), + (v16/*:: as any*/), + (v18/*:: as any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PromptVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*:: as any*/), + (v3/*:: as any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "promptVersions(first:50)" + }, + { + "alias": "pagedVersions", + "args": (v19/*:: as any*/), + "filters": null, + "handle": "connection", + "key": "PromptVersionsList__pagedVersions", + "kind": "LinkedHandle", + "name": "promptVersions" + }, { "alias": null, "args": null, @@ -936,6 +1019,13 @@ return { "plural": false, "selections": (v17/*:: as any*/), "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "versionCount", + "storageKey": null } ], "type": "Prompt", @@ -947,12 +1037,12 @@ return { ] }, "params": { - "cacheID": "2354b1af82b29bf051d563ee4cbd840e", + "cacheID": "1c63e49442fd684f6300cdafe4acb511", "id": null, "metadata": {}, "name": "promptLoaderQuery", "operationKind": "query", - "text": "query promptLoaderQuery(\n $id: ID!\n) {\n prompt: node(id: $id) {\n __typename\n id\n ... on Prompt {\n name\n ...PromptIndexPage__main\n ...PromptVersionsPageContent__main\n ...PromptLayout__main\n }\n }\n}\n\nfragment EditPromptButton_data on Prompt {\n id\n description\n metadata\n}\n\nfragment PromptChatMessagesCard__main on PromptVersion {\n provider: modelProvider\n template {\n __typename\n ... on PromptChatTemplate {\n messages {\n role\n content {\n __typename\n ... on TextContentPart {\n text {\n text\n }\n }\n ... on ToolCallContentPart {\n toolCall {\n toolCallId\n toolCall {\n arguments\n name\n }\n }\n }\n ... on ToolResultContentPart {\n toolResult {\n toolCallId\n result\n }\n }\n }\n }\n }\n ... on PromptStringTemplate {\n template\n }\n }\n templateType\n templateFormat\n}\n\nfragment PromptCodeExportCard__main on PromptVersion {\n id\n invocationParameters {\n __typename\n ...PromptInvocationParametersReadableFragment\n }\n modelName\n modelProvider\n responseFormat {\n jsonSchema {\n name\n description\n schema\n strict\n }\n }\n tools {\n tools {\n __typename\n ... on PromptToolFunction {\n function {\n name\n description\n parameters\n strict\n }\n }\n ... on PromptToolRaw {\n raw\n }\n }\n toolChoice {\n type\n functionName\n }\n disableParallelToolCalls\n }\n template {\n __typename\n ... on PromptChatTemplate {\n messages {\n role\n content {\n __typename\n ... on TextContentPart {\n __typename\n text {\n text\n }\n }\n ... on ToolCallContentPart {\n __typename\n toolCall {\n toolCallId\n toolCall {\n name\n arguments\n }\n }\n }\n ... on ToolResultContentPart {\n __typename\n toolResult {\n toolCallId\n result\n }\n }\n }\n }\n }\n ... on PromptStringTemplate {\n template\n }\n }\n templateFormat\n templateType\n}\n\nfragment PromptIndexPage__aside on Prompt {\n id\n name\n description\n metadata\n ...PromptLatestVersionsListFragment\n ...EditPromptButton_data\n ...PromptLabels\n}\n\nfragment PromptIndexPage__main on Prompt {\n promptVersions {\n edges {\n node {\n ...PromptInvocationParameters__main\n ...PromptChatMessagesCard__main\n ...PromptCodeExportCard__main\n ...PromptModelConfigurationCard__main\n id\n }\n }\n }\n ...PromptIndexPage__aside\n}\n\nfragment PromptInvocationParametersReadableFragment on PromptInvocationParameters {\n __isPromptInvocationParameters: __typename\n __typename\n ... on PromptOpenAIInvocationParameters {\n temperature\n openaiMaxTokens: maxTokens\n maxCompletionTokens\n frequencyPenalty\n presencePenalty\n topP\n seed\n stop\n reasoningEffort\n extraBody\n }\n ... on PromptAnthropicInvocationParameters {\n anthropicMaxTokens: maxTokens\n temperature\n topP\n stopSequences\n outputConfig {\n effort\n }\n thinking {\n __typename\n ... on PromptAnthropicThinkingDisabled {\n disabled\n }\n ... on PromptAnthropicThinkingEnabled {\n budgetTokens\n enabledDisplay: display\n }\n ... on PromptAnthropicThinkingAdaptive {\n adaptiveDisplay: display\n }\n }\n extraBody\n }\n ... on PromptGoogleInvocationParameters {\n temperature\n maxOutputTokens\n stopSequences\n presencePenalty\n frequencyPenalty\n topP\n topK\n thinkingConfig {\n thinkingBudget\n thinkingLevel\n includeThoughts\n }\n }\n ... on PromptAwsInvocationParameters {\n awsMaxTokens: maxTokens\n temperature\n topP\n stopSequences\n }\n}\n\nfragment PromptInvocationParameters__main on PromptVersion {\n invocationParameters {\n __typename\n ...PromptInvocationParametersReadableFragment\n }\n tools {\n toolChoice {\n type\n functionName\n }\n }\n}\n\nfragment PromptLLM__main on PromptVersion {\n model: modelName\n provider: modelProvider\n}\n\nfragment PromptLabels on Prompt {\n labels {\n name\n color\n id\n }\n}\n\nfragment PromptLatestVersionsListFragment on Prompt {\n latestVersions: promptVersions(first: 5) {\n edges {\n version: node {\n id\n description\n createdAt\n sequenceNumber\n ...PromptVersionSummaryFragment\n }\n }\n }\n}\n\nfragment PromptLayout__main on Prompt {\n id\n name\n description\n metadata\n sourcePrompt {\n id\n name\n }\n promptVersions {\n edges {\n node {\n id\n }\n }\n }\n}\n\nfragment PromptModelConfigurationCard__main on PromptVersion {\n model: modelName\n provider: modelProvider\n ...PromptLLM__main\n ...PromptInvocationParameters__main\n ...PromptTools__main\n ...PromptResponseFormatFragment\n}\n\nfragment PromptResponseFormatFragment on PromptVersion {\n responseFormat {\n jsonSchema {\n name\n description\n schema\n strict\n }\n }\n}\n\nfragment PromptTools__main on PromptVersion {\n tools {\n tools {\n __typename\n ... on PromptToolFunction {\n function {\n name\n description\n parameters\n strict\n }\n }\n ... on PromptToolRaw {\n raw\n }\n }\n }\n}\n\nfragment PromptVersionSummaryFragment on PromptVersion {\n id\n description\n sequenceNumber\n createdAt\n user {\n id\n username\n profilePictureUrl\n }\n ...PromptVersionTagsList_data\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n\nfragment PromptVersionsList__main on Prompt {\n promptVersions {\n edges {\n version: node {\n id\n ...PromptVersionSummaryFragment\n }\n }\n }\n}\n\nfragment PromptVersionsPageContent__main on Prompt {\n ...PromptVersionsList__main\n}\n" + "text": "query promptLoaderQuery(\n $id: ID!\n) {\n prompt: node(id: $id) {\n __typename\n id\n ... on Prompt {\n name\n ...PromptIndexPage__main\n ...PromptVersionsPageContent__main\n ...PromptLayout__main\n }\n }\n}\n\nfragment EditPromptButton_data on Prompt {\n id\n description\n metadata\n}\n\nfragment PromptChatMessagesCard__main on PromptVersion {\n provider: modelProvider\n template {\n __typename\n ... on PromptChatTemplate {\n messages {\n role\n content {\n __typename\n ... on TextContentPart {\n text {\n text\n }\n }\n ... on ToolCallContentPart {\n toolCall {\n toolCallId\n toolCall {\n arguments\n name\n }\n }\n }\n ... on ToolResultContentPart {\n toolResult {\n toolCallId\n result\n }\n }\n }\n }\n }\n ... on PromptStringTemplate {\n template\n }\n }\n templateType\n templateFormat\n}\n\nfragment PromptCodeExportCard__main on PromptVersion {\n id\n invocationParameters {\n __typename\n ...PromptInvocationParametersReadableFragment\n }\n modelName\n modelProvider\n responseFormat {\n jsonSchema {\n name\n description\n schema\n strict\n }\n }\n tools {\n tools {\n __typename\n ... on PromptToolFunction {\n function {\n name\n description\n parameters\n strict\n }\n }\n ... on PromptToolRaw {\n raw\n }\n }\n toolChoice {\n type\n functionName\n }\n disableParallelToolCalls\n }\n template {\n __typename\n ... on PromptChatTemplate {\n messages {\n role\n content {\n __typename\n ... on TextContentPart {\n __typename\n text {\n text\n }\n }\n ... on ToolCallContentPart {\n __typename\n toolCall {\n toolCallId\n toolCall {\n name\n arguments\n }\n }\n }\n ... on ToolResultContentPart {\n __typename\n toolResult {\n toolCallId\n result\n }\n }\n }\n }\n }\n ... on PromptStringTemplate {\n template\n }\n }\n templateFormat\n templateType\n}\n\nfragment PromptIndexPage__aside on Prompt {\n id\n name\n description\n metadata\n ...PromptLatestVersionsListFragment\n ...EditPromptButton_data\n ...PromptLabels\n}\n\nfragment PromptIndexPage__main on Prompt {\n promptVersions {\n edges {\n node {\n ...PromptInvocationParameters__main\n ...PromptChatMessagesCard__main\n ...PromptCodeExportCard__main\n ...PromptModelConfigurationCard__main\n id\n }\n }\n }\n ...PromptIndexPage__aside\n}\n\nfragment PromptInvocationParametersReadableFragment on PromptInvocationParameters {\n __isPromptInvocationParameters: __typename\n __typename\n ... on PromptOpenAIInvocationParameters {\n temperature\n openaiMaxTokens: maxTokens\n maxCompletionTokens\n frequencyPenalty\n presencePenalty\n topP\n seed\n stop\n reasoningEffort\n extraBody\n }\n ... on PromptAnthropicInvocationParameters {\n anthropicMaxTokens: maxTokens\n temperature\n topP\n stopSequences\n outputConfig {\n effort\n }\n thinking {\n __typename\n ... on PromptAnthropicThinkingDisabled {\n disabled\n }\n ... on PromptAnthropicThinkingEnabled {\n budgetTokens\n enabledDisplay: display\n }\n ... on PromptAnthropicThinkingAdaptive {\n adaptiveDisplay: display\n }\n }\n extraBody\n }\n ... on PromptGoogleInvocationParameters {\n temperature\n maxOutputTokens\n stopSequences\n presencePenalty\n frequencyPenalty\n topP\n topK\n thinkingConfig {\n thinkingBudget\n thinkingLevel\n includeThoughts\n }\n }\n ... on PromptAwsInvocationParameters {\n awsMaxTokens: maxTokens\n temperature\n topP\n stopSequences\n }\n}\n\nfragment PromptInvocationParameters__main on PromptVersion {\n invocationParameters {\n __typename\n ...PromptInvocationParametersReadableFragment\n }\n tools {\n toolChoice {\n type\n functionName\n }\n }\n}\n\nfragment PromptLLM__main on PromptVersion {\n model: modelName\n provider: modelProvider\n}\n\nfragment PromptLabels on Prompt {\n labels {\n name\n color\n id\n }\n}\n\nfragment PromptLatestVersionsListFragment on Prompt {\n latestVersions: promptVersions(first: 5) {\n edges {\n version: node {\n id\n description\n createdAt\n sequenceNumber\n ...PromptVersionSummaryFragment\n }\n }\n }\n}\n\nfragment PromptLayout__main on Prompt {\n id\n name\n description\n metadata\n sourcePrompt {\n id\n name\n }\n versionCount\n}\n\nfragment PromptModelConfigurationCard__main on PromptVersion {\n model: modelName\n provider: modelProvider\n ...PromptLLM__main\n ...PromptInvocationParameters__main\n ...PromptTools__main\n ...PromptResponseFormatFragment\n}\n\nfragment PromptResponseFormatFragment on PromptVersion {\n responseFormat {\n jsonSchema {\n name\n description\n schema\n strict\n }\n }\n}\n\nfragment PromptTools__main on PromptVersion {\n tools {\n tools {\n __typename\n ... on PromptToolFunction {\n function {\n name\n description\n parameters\n strict\n }\n }\n ... on PromptToolRaw {\n raw\n }\n }\n }\n}\n\nfragment PromptVersionSummaryFragment on PromptVersion {\n id\n description\n sequenceNumber\n createdAt\n user {\n id\n username\n profilePictureUrl\n }\n ...PromptVersionTagsList_data\n}\n\nfragment PromptVersionTagsList_data on PromptVersion {\n tags {\n id\n name\n }\n}\n\nfragment PromptVersionsList__main on Prompt {\n pagedVersions: promptVersions(first: 50) {\n edges {\n version: node {\n id\n ...PromptVersionSummaryFragment\n }\n cursor\n node {\n __typename\n id\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n\nfragment PromptVersionsPageContent__main on Prompt {\n ...PromptVersionsList__main\n}\n" } }; })(); diff --git a/js/app/tests/prompt-management.spec.ts b/js/app/tests/prompt-management.spec.ts index a9f4c34b49a..f327789656b 100644 --- a/js/app/tests/prompt-management.spec.ts +++ b/js/app/tests/prompt-management.spec.ts @@ -152,4 +152,54 @@ test.describe("Prompt Management", () => { await expect(page.getByRole("heading", { name: promptName })).toBeVisible(); }); + + test("versions tab lists every version of a prompt with more than one page of versions", async ({ + page, + }) => { + const promptName = `many-versions-${randomUUID().slice(0, 8)}`; + // The versions list loads 50 versions per page, so this needs a second page + const numVersions = 60; + for (let versionIndex = 1; versionIndex <= numVersions; versionIndex++) { + const response = await page.request.post("/v1/prompts", { + data: { + prompt: { name: promptName }, + version: { + description: `version ${versionIndex}`, + model_provider: "OPENAI", + model_name: "gpt-4o-mini", + template_type: "CHAT", + template_format: "MUSTACHE", + template: { + type: "chat", + messages: [{ role: "user", content: `message ${versionIndex}` }], + }, + invocation_parameters: { type: "openai", openai: {} }, + }, + }, + }); + expect(response.ok()).toBeTruthy(); + } + + await page.goto("/prompts"); + await page.getByRole("link", { name: promptName }).click(); + await expect(page.getByRole("heading", { name: promptName })).toBeVisible(); + const versionsTab = page.getByRole("tab", { name: /versions/i }); + await expect(versionsTab).toContainText(String(numVersions)); + await versionsTab.click(); + + const versionLinks = page.locator("a[href*='/versions/']"); + await expect(versionLinks.first()).toBeVisible(); + await expect + .poll(async () => { + await versionLinks.last().scrollIntoViewIfNeeded(); + return versionLinks.count(); + }) + .toBe(numVersions); + + await versionLinks.last().click(); + await expect(page.getByTestId("prompt-chat-messages-card")).toContainText( + "message 1" + ); + await expect(versionLinks).toHaveCount(numVersions); + }); });