Compare commits

..

1 Commits

Author SHA1 Message Date
594c89ab76 Select the detection audit payload on getTableState
All checks were successful
Tests / Tests (pull_request) Successful in 3m9s
Adds identifierToBox to TableStateGQL and selects it, alongside the
homography that was already in the schema but never requested, so the
photo path can draw a detection back over the image it came from. The
live and shot queries keep the layout-only selection: their frame lives
inside a video the app never holds, and one of them polls every five
seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:55:19 -07:00
4 changed files with 272 additions and 188 deletions

View File

@@ -2579,7 +2579,6 @@ export type Mutation = {
startChallenge: ChallengeEntry; startChallenge: ChallengeEntry;
submitCancellationFeedback: Scalars["Boolean"]["output"]; submitCancellationFeedback: Scalars["Boolean"]["output"];
submitChallengeEntry: ChallengeEntry; submitChallengeEntry: ChallengeEntry;
submitSessionCoachFeedback: SessionCoachFeedbackGql;
syncAppleSubscription: SyncAppleSubscriptionResultGql; syncAppleSubscription: SyncAppleSubscriptionResultGql;
undismissChallenge: Scalars["Boolean"]["output"]; undismissChallenge: Scalars["Boolean"]["output"];
unfollowUser: UserGql; unfollowUser: UserGql;
@@ -2841,12 +2840,6 @@ export type MutationSubmitChallengeEntryArgs = {
videoId: Scalars["ID"]["input"]; videoId: Scalars["ID"]["input"];
}; };
export type MutationSubmitSessionCoachFeedbackArgs = {
generationId: Scalars["ID"]["input"];
response: SessionCoachFeedbackResponseEnum;
videoId: Scalars["Int"]["input"];
};
export type MutationSyncAppleSubscriptionArgs = { export type MutationSyncAppleSubscriptionArgs = {
input: SyncAppleSubscriptionInputGql; input: SyncAppleSubscriptionInputGql;
}; };
@@ -3692,19 +3685,6 @@ export type SessionCoachEvidenceGql = {
score: Scalars["Float"]["output"]; score: Scalars["Float"]["output"];
}; };
export type SessionCoachFeedbackGql = {
__typename?: "SessionCoachFeedbackGQL";
response: SessionCoachFeedbackResponseEnum;
updatedAt: Scalars["DateTime"]["output"];
};
export enum SessionCoachFeedbackResponseEnum {
AlreadyWorkingOnThis = "ALREADY_WORKING_ON_THIS",
Helpful = "HELPFUL",
NotEnoughEvidence = "NOT_ENOUGH_EVIDENCE",
WrongFocus = "WRONG_FOCUS",
}
export type SessionCoachGql = { export type SessionCoachGql = {
__typename?: "SessionCoachGQL"; __typename?: "SessionCoachGQL";
agentGeneration?: Maybe<SessionCoachGenerationGql>; agentGeneration?: Maybe<SessionCoachGenerationGql>;
@@ -3757,7 +3737,6 @@ export type SessionCoachGenerationGql = {
createdAt: Scalars["DateTime"]["output"]; createdAt: Scalars["DateTime"]["output"];
decision?: Maybe<SessionCoachDecisionGql>; decision?: Maybe<SessionCoachDecisionGql>;
diagnostics?: Maybe<SessionCoachGenerationDiagnosticsGql>; diagnostics?: Maybe<SessionCoachGenerationDiagnosticsGql>;
feedback?: Maybe<SessionCoachFeedbackGql>;
id: Scalars["ID"]["output"]; id: Scalars["ID"]["output"];
state: SessionCoachGenerationStateEnum; state: SessionCoachGenerationStateEnum;
}; };
@@ -4099,6 +4078,7 @@ export type SyncAppleSubscriptionResultGql = {
export type TableStateGql = { export type TableStateGql = {
__typename?: "TableStateGQL"; __typename?: "TableStateGQL";
homography?: Maybe<HomographyInfoGql>; homography?: Maybe<HomographyInfoGql>;
identifierToBox: Array<BoundingBoxGql>;
identifierToColor: Array<Maybe<RgbColorGql>>; identifierToColor: Array<Maybe<RgbColorGql>>;
identifierToPosition: Array<Array<Scalars["Float"]["output"]>>; identifierToPosition: Array<Array<Scalars["Float"]["output"]>>;
}; };
@@ -7225,11 +7205,6 @@ export type GetSessionCoachQuery = {
abstentionReason?: string | null; abstentionReason?: string | null;
synthesisRationale?: string | null; synthesisRationale?: string | null;
} | null; } | null;
feedback?: {
__typename?: "SessionCoachFeedbackGQL";
response: SessionCoachFeedbackResponseEnum;
updatedAt: any;
} | null;
} | null; } | null;
}; };
}; };
@@ -7287,27 +7262,7 @@ export type RequestSessionCoachGenerationMutation = {
abstentionReason?: string | null; abstentionReason?: string | null;
synthesisRationale?: string | null; synthesisRationale?: string | null;
} | null; } | null;
feedback?: {
__typename?: "SessionCoachFeedbackGQL";
response: SessionCoachFeedbackResponseEnum;
updatedAt: any;
} | null; } | null;
} | null;
};
};
export type SubmitSessionCoachFeedbackMutationVariables = Exact<{
videoId: Scalars["Int"]["input"];
generationId: Scalars["ID"]["input"];
response: SessionCoachFeedbackResponseEnum;
}>;
export type SubmitSessionCoachFeedbackMutation = {
__typename?: "Mutation";
submitSessionCoachFeedback: {
__typename?: "SessionCoachFeedbackGQL";
response: SessionCoachFeedbackResponseEnum;
updatedAt: any;
}; };
}; };
@@ -7388,11 +7343,6 @@ export type SessionCoachGenerationFieldsFragment = {
abstentionReason?: string | null; abstentionReason?: string | null;
synthesisRationale?: string | null; synthesisRationale?: string | null;
} | null; } | null;
feedback?: {
__typename?: "SessionCoachFeedbackGQL";
response: SessionCoachFeedbackResponseEnum;
updatedAt: any;
} | null;
}; };
export type CreateShotShareLinkMutationVariables = Exact<{ export type CreateShotShareLinkMutationVariables = Exact<{
@@ -7531,6 +7481,76 @@ export type FinalizePlayerAssignmentsMutation = {
}>; }>;
}; };
export type TableStateLayoutFieldsFragment = {
__typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{ __typename?: "RGBColorGQL"; hex: string } | null>;
};
export type DetectionBoxFieldsFragment = {
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
};
export type DetectionPocketPointFieldsFragment = {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
export type TableStateDetectionFieldsFragment = {
__typename?: "TableStateGQL";
identifierToBox: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
homography?: {
__typename?: "HomographyInfoGQL";
crop: {
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
};
pockets: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
sourcePoints: {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
destPoints: {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
} | null;
};
export type GetTableStateQueryVariables = Exact<{ export type GetTableStateQueryVariables = Exact<{
b64Image: Scalars["String"]["input"]; b64Image: Scalars["String"]["input"];
tableSize?: InputMaybe<Scalars["Float"]["input"]>; tableSize?: InputMaybe<Scalars["Float"]["input"]>;
@@ -7546,6 +7566,48 @@ export type GetTableStateQuery = {
__typename?: "RGBColorGQL"; __typename?: "RGBColorGQL";
hex: string; hex: string;
} | null>; } | null>;
identifierToBox: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
homography?: {
__typename?: "HomographyInfoGQL";
crop: {
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
};
pockets: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
sourcePoints: {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
destPoints: {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
} | null;
}; };
}; };
@@ -10000,10 +10062,6 @@ export const SessionCoachGenerationFieldsFragmentDoc = gql`
abstentionReason abstentionReason
synthesisRationale synthesisRationale
} }
feedback {
response
updatedAt
}
} }
`; `;
export const PlayerClusterShotFieldsFragmentDoc = gql` export const PlayerClusterShotFieldsFragmentDoc = gql`
@@ -10035,6 +10093,73 @@ export const PlayerClusterFieldsFragmentDoc = gql`
} }
${PlayerClusterShotFieldsFragmentDoc} ${PlayerClusterShotFieldsFragmentDoc}
`; `;
export const TableStateLayoutFieldsFragmentDoc = gql`
fragment TableStateLayoutFields on TableStateGQL {
identifierToPosition
identifierToColor {
hex
}
}
`;
export const DetectionBoxFieldsFragmentDoc = gql`
fragment DetectionBoxFields on BoundingBoxGQL {
left
top
width
height
}
`;
export const DetectionPocketPointFieldsFragmentDoc = gql`
fragment DetectionPocketPointFields on PocketPointsGQL {
topLeft {
x
y
}
topSide {
x
y
}
topRight {
x
y
}
bottomLeft {
x
y
}
bottomSide {
x
y
}
bottomRight {
x
y
}
}
`;
export const TableStateDetectionFieldsFragmentDoc = gql`
fragment TableStateDetectionFields on TableStateGQL {
identifierToBox {
...DetectionBoxFields
}
homography {
crop {
...DetectionBoxFields
}
pockets {
...DetectionBoxFields
}
sourcePoints {
...DetectionPocketPointFields
}
destPoints {
...DetectionPocketPointFields
}
}
}
${DetectionBoxFieldsFragmentDoc}
${DetectionPocketPointFieldsFragmentDoc}
`;
export const ShotClipRangeFragmentDoc = gql` export const ShotClipRangeFragmentDoc = gql`
fragment ShotClipRange on ShotGQL { fragment ShotClipRange on ShotGQL {
id id
@@ -15792,68 +15917,6 @@ export type RequestSessionCoachGenerationMutationOptions =
RequestSessionCoachGenerationMutation, RequestSessionCoachGenerationMutation,
RequestSessionCoachGenerationMutationVariables RequestSessionCoachGenerationMutationVariables
>; >;
export const SubmitSessionCoachFeedbackDocument = gql`
mutation SubmitSessionCoachFeedback(
$videoId: Int!
$generationId: ID!
$response: SessionCoachFeedbackResponseEnum!
) {
submitSessionCoachFeedback(
videoId: $videoId
generationId: $generationId
response: $response
) {
response
updatedAt
}
}
`;
export type SubmitSessionCoachFeedbackMutationFn = Apollo.MutationFunction<
SubmitSessionCoachFeedbackMutation,
SubmitSessionCoachFeedbackMutationVariables
>;
/**
* __useSubmitSessionCoachFeedbackMutation__
*
* To run a mutation, you first call `useSubmitSessionCoachFeedbackMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSubmitSessionCoachFeedbackMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [submitSessionCoachFeedbackMutation, { data, loading, error }] = useSubmitSessionCoachFeedbackMutation({
* variables: {
* videoId: // value for 'videoId'
* generationId: // value for 'generationId'
* response: // value for 'response'
* },
* });
*/
export function useSubmitSessionCoachFeedbackMutation(
baseOptions?: Apollo.MutationHookOptions<
SubmitSessionCoachFeedbackMutation,
SubmitSessionCoachFeedbackMutationVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<
SubmitSessionCoachFeedbackMutation,
SubmitSessionCoachFeedbackMutationVariables
>(SubmitSessionCoachFeedbackDocument, options);
}
export type SubmitSessionCoachFeedbackMutationHookResult = ReturnType<
typeof useSubmitSessionCoachFeedbackMutation
>;
export type SubmitSessionCoachFeedbackMutationResult =
Apollo.MutationResult<SubmitSessionCoachFeedbackMutation>;
export type SubmitSessionCoachFeedbackMutationOptions =
Apollo.BaseMutationOptions<
SubmitSessionCoachFeedbackMutation,
SubmitSessionCoachFeedbackMutationVariables
>;
export const CreateShotShareLinkDocument = gql` export const CreateShotShareLinkDocument = gql`
mutation CreateShotShareLink( mutation CreateShotShareLink(
$shotIds: [Int!]! $shotIds: [Int!]!
@@ -16048,12 +16111,12 @@ export const GetTableStateDocument = gql`
tableSize: $tableSize tableSize: $tableSize
useHomography: $useHomography useHomography: $useHomography
) { ) {
identifierToPosition ...TableStateLayoutFields
identifierToColor { ...TableStateDetectionFields
hex
}
} }
} }
${TableStateLayoutFieldsFragmentDoc}
${TableStateDetectionFieldsFragmentDoc}
`; `;
/** /**
@@ -16129,13 +16192,11 @@ export const GetLiveTableStateDocument = gql`
videoId videoId
frameIndex frameIndex
tableState { tableState {
identifierToPosition ...TableStateLayoutFields
identifierToColor {
hex
}
} }
} }
} }
${TableStateLayoutFieldsFragmentDoc}
`; `;
/** /**
@@ -16210,13 +16271,11 @@ export const GetShotTableStateDocument = gql`
videoId videoId
frameIndex frameIndex
tableState { tableState {
identifierToPosition ...TableStateLayoutFields
identifierToColor {
hex
}
} }
} }
} }
${TableStateLayoutFieldsFragmentDoc}
`; `;
/** /**

View File

@@ -45,21 +45,6 @@ mutation RequestSessionCoachGeneration($videoId: Int!) {
} }
} }
mutation SubmitSessionCoachFeedback(
$videoId: Int!
$generationId: ID!
$response: SessionCoachFeedbackResponseEnum!
) {
submitSessionCoachFeedback(
videoId: $videoId
generationId: $generationId
response: $response
) {
response
updatedAt
}
}
fragment SessionCoachCandidateFields on SessionCoachCandidateGQL { fragment SessionCoachCandidateFields on SessionCoachCandidateGQL {
id id
rank rank
@@ -132,8 +117,4 @@ fragment SessionCoachGenerationFields on SessionCoachGenerationGQL {
abstentionReason abstentionReason
synthesisRationale synthesisRationale
} }
feedback {
response
updatedAt
}
} }

View File

@@ -1,3 +1,72 @@
fragment TableStateLayoutFields on TableStateGQL {
identifierToPosition
identifierToColor {
hex
}
}
fragment DetectionBoxFields on BoundingBoxGQL {
left
top
width
height
}
fragment DetectionPocketPointFields on PocketPointsGQL {
topLeft {
x
y
}
topSide {
x
y
}
topRight {
x
y
}
bottomLeft {
x
y
}
bottomSide {
x
y
}
bottomRight {
x
y
}
}
# Everything needed to redraw a detection over the image it came from:
# the detector's boxes plus the homography that turned them into plane
# positions. Boxes, pocket boxes, and source points share the crop's
# coordinate space; the crop locates that space in the full frame.
#
# Only the photo query selects this. The live and shot queries read a
# frame out of a video the app never holds, so there is no image to
# overlay, and getLiveTableState is polled every five seconds.
fragment TableStateDetectionFields on TableStateGQL {
identifierToBox {
...DetectionBoxFields
}
homography {
crop {
...DetectionBoxFields
}
pockets {
...DetectionBoxFields
}
sourcePoints {
...DetectionPocketPointFields
}
destPoints {
...DetectionPocketPointFields
}
}
}
query GetTableState( query GetTableState(
$b64Image: String! $b64Image: String!
$tableSize: Float $tableSize: Float
@@ -8,10 +77,8 @@ query GetTableState(
tableSize: $tableSize tableSize: $tableSize
useHomography: $useHomography useHomography: $useHomography
) { ) {
identifierToPosition ...TableStateLayoutFields
identifierToColor { ...TableStateDetectionFields
hex
}
} }
} }
@@ -20,10 +87,7 @@ query GetLiveTableState($videoId: Int!) {
videoId videoId
frameIndex frameIndex
tableState { tableState {
identifierToPosition ...TableStateLayoutFields
identifierToColor {
hex
}
} }
} }
} }
@@ -34,10 +98,7 @@ query GetShotTableState($shotId: Int!) {
videoId videoId
frameIndex frameIndex
tableState { tableState {
identifierToPosition ...TableStateLayoutFields
identifierToColor {
hex
}
} }
} }
} }

View File

@@ -249,6 +249,11 @@ 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
@@ -259,11 +264,6 @@ input CreatePoolHallInput {
timezone: String = null timezone: String = null
} }
input CreateShotShareLinkInput {
shotIds: [Int!]!
paddingSeconds: Float = null
}
type CreateSubscriptionResultGQL { type CreateSubscriptionResultGQL {
checkoutUrl: String! checkoutUrl: String!
sessionId: String! sessionId: String!
@@ -728,11 +728,6 @@ type Mutation {
shotId: Int! shotId: Int!
fieldsToEdit: EditableShotFieldInputGQL! fieldsToEdit: EditableShotFieldInputGQL!
): EditShotReturn! ): EditShotReturn!
submitSessionCoachFeedback(
videoId: Int!
generationId: ID!
response: SessionCoachFeedbackResponseEnum!
): SessionCoachFeedbackGQL!
requestSessionCoachGeneration(videoId: Int!): SessionCoachGQL! requestSessionCoachGeneration(videoId: Int!): SessionCoachGQL!
getProfileImageUploadLink( getProfileImageUploadLink(
fileExt: String = ".png" fileExt: String = ".png"
@@ -1392,18 +1387,6 @@ type SessionCoachEvidenceGQL {
baselineMakePercentage: Float baselineMakePercentage: Float
} }
type SessionCoachFeedbackGQL {
response: SessionCoachFeedbackResponseEnum!
updatedAt: DateTime!
}
enum SessionCoachFeedbackResponseEnum {
HELPFUL
WRONG_FOCUS
ALREADY_WORKING_ON_THIS
NOT_ENOUGH_EVIDENCE
}
type SessionCoachGQL { type SessionCoachGQL {
videoId: Int! videoId: Int!
processingId: Int processingId: Int
@@ -1455,7 +1438,6 @@ type SessionCoachGenerationGQL {
completedAt: DateTime completedAt: DateTime
decision: SessionCoachDecisionGQL decision: SessionCoachDecisionGQL
diagnostics: SessionCoachGenerationDiagnosticsGQL diagnostics: SessionCoachGenerationDiagnosticsGQL
feedback: SessionCoachFeedbackGQL
} }
enum SessionCoachGenerationStateEnum { enum SessionCoachGenerationStateEnum {
@@ -1477,11 +1459,6 @@ enum SessionCoachStateEnum {
FAILED FAILED
} }
type ShareLinkGQL {
slug: String!
url: String!
}
type ShotAnnotationGQL { type ShotAnnotationGQL {
shotId: Int! shotId: Int!
type: ShotAnnotationTypeGQL! type: ShotAnnotationTypeGQL!
@@ -1731,6 +1708,7 @@ type TableStateGQL {
identifierToPosition: [[Float!]!]! identifierToPosition: [[Float!]!]!
homography: HomographyInfoGQL homography: HomographyInfoGQL
identifierToColor: [RGBColorGQL]! identifierToColor: [RGBColorGQL]!
identifierToBox: [BoundingBoxGQL!]!
} }
type TagClassGQL { type TagClassGQL {
@@ -1942,6 +1920,11 @@ type UserSubscriptionStatusGQL {
stripeSubscriptionId: String stripeSubscriptionId: String
} }
type ShareLinkGQL {
slug: String!
url: String!
}
type VideoExportJobGQL { type VideoExportJobGQL {
id: Int! id: Int!
videoId: Int! videoId: Int!