Compare commits

...

3 Commits

Author SHA1 Message Date
9b6b08c1ec Return the shot's start frame image from getShotTableState
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Shot Lab was showing the video's single thumbnail next to every shot's
layout. The backend now returns the frame the layout was detected in as
a base64 JPEG (frameImage), selected by the GetShotTableState operation.
Also dedupes ShareLinkGQL/CreateShotShareLinkInput, which appeared twice
in the committed schema after a bad merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:56:38 -07:00
0f01c71a43 Merge pull request 'Add Session Coach generation status query' (#320) from loewy/session-coach-status-polling-gql into master
Reviewed-on: #320
2026-08-06 06:40:20 +00:00
238ade1384 Add Session Coach generation status query
All checks were successful
Tests / Tests (pull_request) Successful in 40s
2026-08-05 12:08:51 -07:00
4 changed files with 164 additions and 13 deletions

View File

@@ -3132,6 +3132,7 @@ export type Query = {
getResolvedTier: ResolvedTierGql; getResolvedTier: ResolvedTierGql;
getRuns: GetRunsResult; getRuns: GetRunsResult;
getSessionCoach: SessionCoachGql; getSessionCoach: SessionCoachGql;
getSessionCoachGenerationStatus?: Maybe<SessionCoachGenerationStatusGql>;
getShotAnnotationTypes: Array<ShotAnnotationTypeGql>; getShotAnnotationTypes: Array<ShotAnnotationTypeGql>;
getShotTableState: ShotTableStateGql; getShotTableState: ShotTableStateGql;
getShots: Array<ShotGql>; getShots: Array<ShotGql>;
@@ -3293,6 +3294,11 @@ export type QueryGetSessionCoachArgs = {
videoId: Scalars["Int"]["input"]; videoId: Scalars["Int"]["input"];
}; };
export type QueryGetSessionCoachGenerationStatusArgs = {
generationId: Scalars["ID"]["input"];
videoId: Scalars["Int"]["input"];
};
export type QueryGetShotAnnotationTypesArgs = { export type QueryGetShotAnnotationTypesArgs = {
errorTypes?: InputMaybe<Scalars["Boolean"]["input"]>; errorTypes?: InputMaybe<Scalars["Boolean"]["input"]>;
}; };
@@ -3772,6 +3778,18 @@ export enum SessionCoachGenerationStateEnum {
Running = "RUNNING", Running = "RUNNING",
} }
export type SessionCoachGenerationStatusGql = {
__typename?: "SessionCoachGenerationStatusGQL";
completedAt?: Maybe<Scalars["DateTime"]["output"]>;
createdAt: Scalars["DateTime"]["output"];
enqueueCount: Scalars["Int"]["output"];
id: Scalars["ID"]["output"];
lastEnqueuedAt?: Maybe<Scalars["DateTime"]["output"]>;
startedAt?: Maybe<Scalars["DateTime"]["output"]>;
state: SessionCoachGenerationStateEnum;
updatedAt: Scalars["DateTime"]["output"];
};
export enum SessionCoachIneligibilityReasonEnum { export enum SessionCoachIneligibilityReasonEnum {
Multiplayer = "MULTIPLAYER", Multiplayer = "MULTIPLAYER",
} }
@@ -3864,6 +3882,7 @@ export type ShotProjectionGql = {
export type ShotTableStateGql = { export type ShotTableStateGql = {
__typename?: "ShotTableStateGQL"; __typename?: "ShotTableStateGQL";
frameImage?: Maybe<Scalars["String"]["output"]>;
frameIndex: Scalars["Int"]["output"]; frameIndex: Scalars["Int"]["output"];
shotId: Scalars["Int"]["output"]; shotId: Scalars["Int"]["output"];
tableState: TableStateGql; tableState: TableStateGql;
@@ -7238,6 +7257,26 @@ export type GetSessionCoachQuery = {
}; };
}; };
export type GetSessionCoachGenerationStatusQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
generationId: Scalars["ID"]["input"];
}>;
export type GetSessionCoachGenerationStatusQuery = {
__typename?: "Query";
getSessionCoachGenerationStatus?: {
__typename?: "SessionCoachGenerationStatusGQL";
id: string;
state: SessionCoachGenerationStateEnum;
createdAt: any;
updatedAt: any;
startedAt?: any | null;
lastEnqueuedAt?: any | null;
enqueueCount: number;
completedAt?: any | null;
} | null;
};
export type RequestSessionCoachGenerationMutationVariables = Exact<{ export type RequestSessionCoachGenerationMutationVariables = Exact<{
videoId: Scalars["Int"]["input"]; videoId: Scalars["Int"]["input"];
}>; }>;
@@ -7697,6 +7736,7 @@ export type GetShotTableStateQuery = {
shotId: number; shotId: number;
videoId: number; videoId: number;
frameIndex: number; frameIndex: number;
frameImage?: string | null;
tableState: { tableState: {
__typename?: "TableStateGQL"; __typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>; identifierToPosition: Array<Array<number>>;
@@ -15923,6 +15963,90 @@ export type GetSessionCoachQueryResult = Apollo.QueryResult<
GetSessionCoachQuery, GetSessionCoachQuery,
GetSessionCoachQueryVariables GetSessionCoachQueryVariables
>; >;
export const GetSessionCoachGenerationStatusDocument = gql`
query GetSessionCoachGenerationStatus($videoId: Int!, $generationId: ID!) {
getSessionCoachGenerationStatus(
videoId: $videoId
generationId: $generationId
) {
id
state
createdAt
updatedAt
startedAt
lastEnqueuedAt
enqueueCount
completedAt
}
}
`;
/**
* __useGetSessionCoachGenerationStatusQuery__
*
* To run a query within a React component, call `useGetSessionCoachGenerationStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useGetSessionCoachGenerationStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetSessionCoachGenerationStatusQuery({
* variables: {
* videoId: // value for 'videoId'
* generationId: // value for 'generationId'
* },
* });
*/
export function useGetSessionCoachGenerationStatusQuery(
baseOptions: Apollo.QueryHookOptions<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>(GetSessionCoachGenerationStatusDocument, options);
}
export function useGetSessionCoachGenerationStatusLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>(GetSessionCoachGenerationStatusDocument, options);
}
export function useGetSessionCoachGenerationStatusSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>(GetSessionCoachGenerationStatusDocument, options);
}
export type GetSessionCoachGenerationStatusQueryHookResult = ReturnType<
typeof useGetSessionCoachGenerationStatusQuery
>;
export type GetSessionCoachGenerationStatusLazyQueryHookResult = ReturnType<
typeof useGetSessionCoachGenerationStatusLazyQuery
>;
export type GetSessionCoachGenerationStatusSuspenseQueryHookResult = ReturnType<
typeof useGetSessionCoachGenerationStatusSuspenseQuery
>;
export type GetSessionCoachGenerationStatusQueryResult = Apollo.QueryResult<
GetSessionCoachGenerationStatusQuery,
GetSessionCoachGenerationStatusQueryVariables
>;
export const RequestSessionCoachGenerationDocument = gql` export const RequestSessionCoachGenerationDocument = gql`
mutation RequestSessionCoachGeneration($videoId: Int!) { mutation RequestSessionCoachGeneration($videoId: Int!) {
requestSessionCoachGeneration(videoId: $videoId) { requestSessionCoachGeneration(videoId: $videoId) {
@@ -16393,6 +16517,7 @@ export const GetShotTableStateDocument = gql`
shotId shotId
videoId videoId
frameIndex frameIndex
frameImage
tableState { tableState {
...TableStateLayoutFields ...TableStateLayoutFields
} }

View File

@@ -36,6 +36,22 @@ query GetSessionCoach($videoId: Int!) {
} }
} }
query GetSessionCoachGenerationStatus($videoId: Int!, $generationId: ID!) {
getSessionCoachGenerationStatus(
videoId: $videoId
generationId: $generationId
) {
id
state
createdAt
updatedAt
startedAt
lastEnqueuedAt
enqueueCount
completedAt
}
}
mutation RequestSessionCoachGeneration($videoId: Int!) { mutation RequestSessionCoachGeneration($videoId: Int!) {
requestSessionCoachGeneration(videoId: $videoId) { requestSessionCoachGeneration(videoId: $videoId) {
state state

View File

@@ -44,9 +44,10 @@ fragment DetectionPocketPointFields on PocketPointsGQL {
# positions. Boxes, pocket boxes, and source points share the crop's # positions. Boxes, pocket boxes, and source points share the crop's
# coordinate space; the crop locates that space in the full frame. # coordinate space; the crop locates that space in the full frame.
# #
# Only the photo query selects this. The live and shot queries read a # Only the photo query selects this. The shot query returns its source
# frame out of a video the app never holds, so there is no image to # frame (frameImage) but positions there come from the stored homography,
# overlay, and getLiveTableState is polled every five seconds. # so there are no fresh boxes to draw; the live query returns no image at
# all and is polled every five seconds.
fragment TableStateDetectionFields on TableStateGQL { fragment TableStateDetectionFields on TableStateGQL {
identifierToBox { identifierToBox {
...DetectionBoxFields ...DetectionBoxFields
@@ -97,6 +98,9 @@ query GetShotTableState($shotId: Int!) {
shotId shotId
videoId videoId
frameIndex frameIndex
# The b64 JPEG frame the layout was detected in, so the client can show
# the shot's actual starting frame instead of the video's thumbnail.
frameImage
tableState { tableState {
...TableStateLayoutFields ...TableStateLayoutFields
} }

View File

@@ -250,11 +250,6 @@ input CreatePoolHallCameraInput {
claimTermsText: String = null claimTermsText: String = null
} }
input CreateShotShareLinkInput {
shotIds: [Int!]!
paddingSeconds: Float = null
}
input CreatePoolHallInput { input CreatePoolHallInput {
name: String! name: String!
address: String = null address: String = null
@@ -1081,6 +1076,10 @@ type Query {
limit: Int! = 500 limit: Int! = 500
countRespectsLimit: Boolean! = false countRespectsLimit: Boolean! = false
): GetRunsResult! ): GetRunsResult!
getSessionCoachGenerationStatus(
videoId: Int!
generationId: ID!
): SessionCoachGenerationStatusGQL
getSessionCoach(videoId: Int!): SessionCoachGQL! getSessionCoach(videoId: Int!): SessionCoachGQL!
videoPlayerClusters(videoId: Int!): [PlayerClusterGQL!]! videoPlayerClusters(videoId: Int!): [PlayerClusterGQL!]!
getShotAnnotationTypes(errorTypes: Boolean = false): [ShotAnnotationTypeGQL!]! getShotAnnotationTypes(errorTypes: Boolean = false): [ShotAnnotationTypeGQL!]!
@@ -1473,6 +1472,17 @@ enum SessionCoachGenerationStateEnum {
FAILED FAILED
} }
type SessionCoachGenerationStatusGQL {
id: ID!
state: SessionCoachGenerationStateEnum!
createdAt: DateTime!
updatedAt: DateTime!
startedAt: DateTime
lastEnqueuedAt: DateTime
enqueueCount: Int!
completedAt: DateTime
}
enum SessionCoachIneligibilityReasonEnum { enum SessionCoachIneligibilityReasonEnum {
MULTIPLAYER MULTIPLAYER
} }
@@ -1558,6 +1568,7 @@ type ShotTableStateGQL {
videoId: Int! videoId: Int!
frameIndex: Int! frameIndex: Int!
tableState: TableStateGQL! tableState: TableStateGQL!
frameImage: String
} }
input ShotsOrderingComponent @oneOf { input ShotsOrderingComponent @oneOf {
@@ -1951,11 +1962,6 @@ type UserSubscriptionStatusGQL {
stripeSubscriptionId: String stripeSubscriptionId: String
} }
type ShareLinkGQL {
slug: String!
url: String!
}
type VideoExportJobGQL { type VideoExportJobGQL {
id: Int! id: Int!
videoId: Int! videoId: Int!