gergo/web 2155 gendo module multi region (#3528)
* fix(workspaces): allow workspace delete for paid workspaces * feat(gendo): multi region gendo with new api integration and limits * feat(gendo): user credits * feat: adds limits to gendo panel, and terms and conditions link * fix: reworks setting back camera view * feat(gendo): webhook signature verification * fix(gendo): nullability * test(blobstorage): fix test dependency injection --------- Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
This commit is contained in:
@@ -60,7 +60,7 @@ import {
|
||||
ArrowDownTrayIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { useCameraUtilities } from '~/lib/viewer/composables/ui'
|
||||
import type { Vector3 } from 'three'
|
||||
import { Vector3 } from 'three'
|
||||
|
||||
const props = defineProps<{
|
||||
renderRequest: GendoAiRender
|
||||
@@ -108,10 +108,11 @@ const renderUrl = computed(() => {
|
||||
|
||||
const setView = () => {
|
||||
const cam = detailedRender.value?.camera as { target: Vector3; position: Vector3 }
|
||||
|
||||
setViewInternal(
|
||||
{
|
||||
target: cam.target,
|
||||
position: cam.position
|
||||
target: new Vector3(cam.target.x, cam.target.y, cam.target.z),
|
||||
position: new Vector3(cam.position.x, cam.position.y, cam.position.z)
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
>
|
||||
Gendo
|
||||
</CommonTextLink>
|
||||
|
||||
<span class="text-foreground-2"> (Beta)</span>
|
||||
</template>
|
||||
<div class="p-2">
|
||||
@@ -23,38 +24,67 @@
|
||||
placeholder="Your prompt"
|
||||
/>
|
||||
<div class="flex justify-end space-x-2 items-center">
|
||||
<div v-if="limits" class="text-xs text-foreground-2">
|
||||
You have used {{ limits.used }} out of {{ limits.limit }} monthly free
|
||||
renders.
|
||||
</div>
|
||||
<FormButton
|
||||
:disabled="!prompt || isLoading || timeOutWait"
|
||||
v-if="(limits?.used || 1) < (limits?.limit || 0)"
|
||||
:disabled="
|
||||
!prompt ||
|
||||
isLoading ||
|
||||
timeOutWait ||
|
||||
(limits?.used || 0) >= (limits?.limit || 0)
|
||||
"
|
||||
@click="enqueMagic()"
|
||||
>
|
||||
Render
|
||||
</FormButton>
|
||||
<FormButton v-else to="https://gendo.ai?utm=speckle" target="_blank">
|
||||
Visit Gendo
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<ViewerGendoList />
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="text-right grow">
|
||||
<span class="text-foreground-2 text-sm">Learn more about</span>
|
||||
<CommonTextLink
|
||||
text
|
||||
link
|
||||
class="ml-1"
|
||||
to="https://gendo.ai?utm=speckle"
|
||||
target="_blank"
|
||||
>
|
||||
Gendo
|
||||
</CommonTextLink>
|
||||
<div class="flex grow items-center justify-between">
|
||||
<span class="text-foreground-2 text-sm">
|
||||
<CommonTextLink
|
||||
text
|
||||
link
|
||||
class="mr-2"
|
||||
to="https://www.gendo.ai/terms-of-service"
|
||||
target="_blank"
|
||||
>
|
||||
Terms and conditions
|
||||
</CommonTextLink>
|
||||
</span>
|
||||
<div>
|
||||
<span class="text-foreground-2 text-sm">More about</span>
|
||||
<CommonTextLink
|
||||
text
|
||||
link
|
||||
class="ml-1"
|
||||
to="https://gendo.ai?utm=speckle"
|
||||
target="_blank"
|
||||
>
|
||||
Gendo
|
||||
</CommonTextLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ViewerLayoutPanel>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import { useApolloClient, useQuery } from '@vue/apollo-composable'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import { getFirstErrorMessage } from '~/lib/common/helpers/graphql'
|
||||
import { PassReader } from '~/lib/viewer/extensions/PassReader'
|
||||
import { requestGendoAIRender } from '~~/lib/gendo/graphql/queriesAndMutations'
|
||||
import {
|
||||
requestGendoAIRender,
|
||||
activeUserGendoLimits
|
||||
} from '~~/lib/gendo/graphql/queriesAndMutations'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
|
||||
const {
|
||||
@@ -74,6 +104,12 @@ const prompt = ref<string>()
|
||||
const isLoading = ref(false)
|
||||
const timeOutWait = ref(false)
|
||||
|
||||
const { result, refetch } = useQuery(activeUserGendoLimits)
|
||||
|
||||
const limits = computed(() => {
|
||||
return result?.value?.activeUser?.gendoAICredits
|
||||
})
|
||||
|
||||
const enqueMagic = async () => {
|
||||
isLoading.value = true
|
||||
const [depthData, width, height] = await viewerInstance
|
||||
@@ -134,5 +170,6 @@ const lodgeRequest = async (screenshot: string) => {
|
||||
})
|
||||
}
|
||||
isLoading.value = false
|
||||
refetch()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -186,6 +186,7 @@ const documents = {
|
||||
"\n query DeveloperSettingsAuthorizedApps {\n activeUser {\n id\n authorizedApps {\n id\n description\n name\n author {\n id\n name\n avatar\n }\n }\n }\n }\n": types.DeveloperSettingsAuthorizedAppsDocument,
|
||||
"\n query SearchProjects($search: String, $onlyWithRoles: [String!] = null) {\n activeUser {\n projects(limit: 10, filter: { search: $search, onlyWithRoles: $onlyWithRoles }) {\n totalCount\n items {\n ...FormSelectProjects_Project\n }\n }\n }\n }\n": types.SearchProjectsDocument,
|
||||
"\n query SearchProjectModels($search: String, $projectId: String!) {\n project(id: $projectId) {\n id\n models(limit: 10, filter: { search: $search }) {\n totalCount\n items {\n ...FormSelectModels_Model\n }\n }\n }\n }\n": types.SearchProjectModelsDocument,
|
||||
"\n query ActiveUserGendoLimits {\n activeUser {\n id\n gendoAICredits {\n used\n limit\n resetDate\n }\n }\n }\n": types.ActiveUserGendoLimitsDocument,
|
||||
"\n mutation requestGendoAIRender($input: GendoAIRenderInput!) {\n versionMutations {\n requestGendoAIRender(input: $input)\n }\n }\n": types.RequestGendoAiRenderDocument,
|
||||
"\n query GendoAIRender(\n $gendoAiRenderId: String!\n $versionId: String!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n version(id: $versionId) {\n id\n gendoAIRender(id: $gendoAiRenderId) {\n id\n projectId\n modelId\n versionId\n createdAt\n updatedAt\n gendoGenerationId\n status\n prompt\n camera\n responseImage\n user {\n name\n avatar\n id\n }\n }\n }\n }\n }\n": types.GendoAiRenderDocument,
|
||||
"\n query GendoAIRenders($versionId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n version(id: $versionId) {\n id\n gendoAIRenders {\n totalCount\n items {\n id\n createdAt\n updatedAt\n status\n gendoGenerationId\n prompt\n camera\n }\n }\n }\n }\n }\n": types.GendoAiRendersDocument,
|
||||
@@ -1059,6 +1060,10 @@ export function graphql(source: "\n query SearchProjects($search: String, $only
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query SearchProjectModels($search: String, $projectId: String!) {\n project(id: $projectId) {\n id\n models(limit: 10, filter: { search: $search }) {\n totalCount\n items {\n ...FormSelectModels_Model\n }\n }\n }\n }\n"): (typeof documents)["\n query SearchProjectModels($search: String, $projectId: String!) {\n project(id: $projectId) {\n id\n models(limit: 10, filter: { search: $search }) {\n totalCount\n items {\n ...FormSelectModels_Model\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ActiveUserGendoLimits {\n activeUser {\n id\n gendoAICredits {\n used\n limit\n resetDate\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserGendoLimits {\n activeUser {\n id\n gendoAICredits {\n used\n limit\n resetDate\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -3555,6 +3555,7 @@ export type User = {
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
favoriteStreams: StreamCollection;
|
||||
gendoAICredits: UserGendoAiCredits;
|
||||
/** Whether the user has a pending/active email verification token */
|
||||
hasPendingVerification?: Maybe<Scalars['Boolean']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
@@ -3743,6 +3744,13 @@ export type UserEmailMutationsSetPrimaryArgs = {
|
||||
input: SetPrimaryUserEmailInput;
|
||||
};
|
||||
|
||||
export type UserGendoAiCredits = {
|
||||
__typename?: 'UserGendoAICredits';
|
||||
limit: Scalars['Int']['output'];
|
||||
resetDate: Scalars['DateTime']['output'];
|
||||
used: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type UserProjectsFilter = {
|
||||
/** Only include projects where user has the specified roles */
|
||||
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
@@ -4949,6 +4957,11 @@ export type SearchProjectModelsQueryVariables = Exact<{
|
||||
|
||||
export type SearchProjectModelsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, models: { __typename?: 'ModelCollection', totalCount: number, items: Array<{ __typename?: 'Model', id: string, name: string }> } } };
|
||||
|
||||
export type ActiveUserGendoLimitsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ActiveUserGendoLimitsQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, gendoAICredits: { __typename?: 'UserGendoAICredits', used: number, limit: number, resetDate: string } } | null };
|
||||
|
||||
export type RequestGendoAiRenderMutationVariables = Exact<{
|
||||
input: GendoAiRenderInput;
|
||||
}>;
|
||||
@@ -6257,6 +6270,7 @@ export const DeveloperSettingsApplicationsDocument = {"kind":"Document","definit
|
||||
export const DeveloperSettingsAuthorizedAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DeveloperSettingsAuthorizedApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"authorizedApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]}}]} as unknown as DocumentNode<DeveloperSettingsAuthorizedAppsQuery, DeveloperSettingsAuthorizedAppsQueryVariables>;
|
||||
export const SearchProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SearchProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"onlyWithRoles"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"onlyWithRoles"},"value":{"kind":"Variable","name":{"kind":"Name","value":"onlyWithRoles"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormSelectProjects_Project"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FormSelectProjects_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<SearchProjectsQuery, SearchProjectsQueryVariables>;
|
||||
export const SearchProjectModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SearchProjectModels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormSelectModels_Model"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FormSelectModels_Model"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<SearchProjectModelsQuery, SearchProjectModelsQueryVariables>;
|
||||
export const ActiveUserGendoLimitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserGendoLimits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"gendoAICredits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"used"}},{"kind":"Field","name":{"kind":"Name","value":"limit"}},{"kind":"Field","name":{"kind":"Name","value":"resetDate"}}]}}]}}]}}]} as unknown as DocumentNode<ActiveUserGendoLimitsQuery, ActiveUserGendoLimitsQueryVariables>;
|
||||
export const RequestGendoAiRenderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"requestGendoAIRender"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GendoAIRenderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"requestGendoAIRender"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<RequestGendoAiRenderMutation, RequestGendoAiRenderMutationVariables>;
|
||||
export const GendoAiRenderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GendoAIRender"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"gendoAiRenderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"gendoAIRender"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"gendoAiRenderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"gendoGenerationId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"prompt"}},{"kind":"Field","name":{"kind":"Name","value":"camera"}},{"kind":"Field","name":{"kind":"Name","value":"responseImage"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GendoAiRenderQuery, GendoAiRenderQueryVariables>;
|
||||
export const GendoAiRendersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GendoAIRenders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"gendoAIRenders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"gendoGenerationId"}},{"kind":"Field","name":{"kind":"Name","value":"prompt"}},{"kind":"Field","name":{"kind":"Name","value":"camera"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GendoAiRendersQuery, GendoAiRendersQueryVariables>;
|
||||
@@ -6512,6 +6526,7 @@ export type AllObjectTypes = {
|
||||
UserAutomateInfo: UserAutomateInfo,
|
||||
UserEmail: UserEmail,
|
||||
UserEmailMutations: UserEmailMutations,
|
||||
UserGendoAICredits: UserGendoAiCredits,
|
||||
UserProjectsUpdatedMessage: UserProjectsUpdatedMessage,
|
||||
UserSearchResultCollection: UserSearchResultCollection,
|
||||
Version: Version,
|
||||
@@ -7488,6 +7503,7 @@ export type UserFieldArgs = {
|
||||
emails: {},
|
||||
expiredSsoSessions: {},
|
||||
favoriteStreams: UserFavoriteStreamsArgs,
|
||||
gendoAICredits: {},
|
||||
hasPendingVerification: {},
|
||||
id: {},
|
||||
isOnboardingFinished: {},
|
||||
@@ -7523,6 +7539,11 @@ export type UserEmailMutationsFieldArgs = {
|
||||
requestNewEmailVerification: UserEmailMutationsRequestNewEmailVerificationArgs,
|
||||
setPrimary: UserEmailMutationsSetPrimaryArgs,
|
||||
}
|
||||
export type UserGendoAiCreditsFieldArgs = {
|
||||
limit: {},
|
||||
resetDate: {},
|
||||
used: {},
|
||||
}
|
||||
export type UserProjectsUpdatedMessageFieldArgs = {
|
||||
id: {},
|
||||
project: {},
|
||||
@@ -7829,6 +7850,7 @@ export type AllObjectFieldArgTypes = {
|
||||
UserAutomateInfo: UserAutomateInfoFieldArgs,
|
||||
UserEmail: UserEmailFieldArgs,
|
||||
UserEmailMutations: UserEmailMutationsFieldArgs,
|
||||
UserGendoAICredits: UserGendoAiCreditsFieldArgs,
|
||||
UserProjectsUpdatedMessage: UserProjectsUpdatedMessageFieldArgs,
|
||||
UserSearchResultCollection: UserSearchResultCollectionFieldArgs,
|
||||
Version: VersionFieldArgs,
|
||||
|
||||
@@ -2,6 +2,19 @@ import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
// TODO: Clean up these operations and make them component fragment based. Also some of the props requested don't seem to even be used
|
||||
|
||||
export const activeUserGendoLimits = graphql(`
|
||||
query ActiveUserGendoLimits {
|
||||
activeUser {
|
||||
id
|
||||
gendoAICredits {
|
||||
used
|
||||
limit
|
||||
resetDate
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const requestGendoAIRender = graphql(`
|
||||
mutation requestGendoAIRender($input: GendoAIRenderInput!) {
|
||||
versionMutations {
|
||||
|
||||
@@ -378,9 +378,9 @@ export async function init() {
|
||||
app.use(corsMiddleware())
|
||||
// there are some paths, that need the raw body
|
||||
app.use((req, res, next) => {
|
||||
const rawPaths = ['/api/v1/billing/webhooks']
|
||||
if (rawPaths.includes(req.path)) {
|
||||
express.raw({ type: 'application/json' })(req, res, next)
|
||||
const rawPaths = ['/api/v1/billing/webhooks', '/api/thirdparty/gendo/']
|
||||
if (rawPaths.some((p) => req.path.startsWith(p))) {
|
||||
express.raw({ type: 'application/json', limit: '100mb' })(req, res, next)
|
||||
} else {
|
||||
express.json({ limit: '100mb' })(req, res, next)
|
||||
}
|
||||
|
||||
@@ -50,3 +50,13 @@ extend type Subscription {
|
||||
projectVersionGendoAIRenderCreated(id: String!, versionId: String!): GendoAIRender!
|
||||
projectVersionGendoAIRenderUpdated(id: String!, versionId: String!): GendoAIRender!
|
||||
}
|
||||
|
||||
type UserGendoAICredits {
|
||||
used: Int!
|
||||
limit: Int!
|
||||
resetDate: DateTime!
|
||||
}
|
||||
|
||||
extend type User {
|
||||
gendoAICredits: UserGendoAICredits! @isOwner
|
||||
}
|
||||
|
||||
@@ -33,12 +33,6 @@ export type GetBlobMetadataCollection = (params: {
|
||||
}) => Promise<{ blobs: BlobStorageItem[]; cursor: Nullable<string> }>
|
||||
|
||||
export type UploadFileStream = (
|
||||
storeFileStream: (params: {
|
||||
objectKey: string
|
||||
fileStream: Readable | Buffer
|
||||
}) => Promise<{
|
||||
fileHash: string
|
||||
}>,
|
||||
params1: {
|
||||
streamId: string
|
||||
userId: string | undefined
|
||||
@@ -50,3 +44,10 @@ export type UploadFileStream = (
|
||||
fileStream: Readable | Buffer
|
||||
}
|
||||
) => Promise<{ blobId: string; fileName: string; fileHash: string }>
|
||||
|
||||
type FileStream = string | Blob | Readable | Uint8Array | Buffer
|
||||
|
||||
export type StoreFileStream = (args: {
|
||||
objectKey: string
|
||||
fileStream: FileStream
|
||||
}) => Promise<{ fileHash: string }>
|
||||
|
||||
@@ -136,6 +136,7 @@ export const init: SpeckleModule['init'] = async (app) => {
|
||||
const getBlobMetadata = getBlobMetadataFactory({ db: projectDb })
|
||||
|
||||
const uploadFileStream = uploadFileStreamFactory({
|
||||
storeFileStream,
|
||||
upsertBlob: upsertBlobFactory({ db: projectDb }),
|
||||
updateBlob
|
||||
})
|
||||
@@ -178,7 +179,6 @@ export const init: SpeckleModule['init'] = async (app) => {
|
||||
req.log = req.log.child({ blobId })
|
||||
|
||||
uploadOperations[blobId] = uploadFileStream(
|
||||
storeFileStream,
|
||||
{ streamId, userId: req.context.userId },
|
||||
{ blobId, fileName, fileType, fileStream: file }
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
S3ClientConfig,
|
||||
ServiceOutputTypes
|
||||
} from '@aws-sdk/client-s3'
|
||||
import { Upload, Options as UploadOptions } from '@aws-sdk/lib-storage'
|
||||
import { Upload } from '@aws-sdk/lib-storage'
|
||||
import {
|
||||
getS3AccessKey,
|
||||
getS3SecretKey,
|
||||
@@ -27,6 +27,7 @@ import { ensureError, Nullable } from '@speckle/shared'
|
||||
import { get } from 'lodash'
|
||||
import type { Command } from '@aws-sdk/smithy-client'
|
||||
import type stream from 'stream'
|
||||
import { StoreFileStream } from '@/modules/blobstorage/domain/operations'
|
||||
|
||||
let s3Config: Nullable<S3ClientConfig> = null
|
||||
|
||||
@@ -92,13 +93,7 @@ export const getObjectAttributes = async ({ objectKey }: { objectKey: string })
|
||||
return { fileSize: data.ContentLength || 0 }
|
||||
}
|
||||
|
||||
export const storeFileStream = async ({
|
||||
objectKey,
|
||||
fileStream
|
||||
}: {
|
||||
objectKey: string
|
||||
fileStream: UploadOptions['params']['Body']
|
||||
}) => {
|
||||
export const storeFileStream: StoreFileStream = async ({ objectKey, fileStream }) => {
|
||||
const { client, Bucket } = getObjectStorage()
|
||||
const parallelUploads3 = new Upload({
|
||||
client,
|
||||
@@ -111,10 +106,6 @@ export const storeFileStream = async ({
|
||||
leavePartsOnError: false // optional manually handle dropped parts
|
||||
})
|
||||
|
||||
// parallelUploads3.on('httpUploadProgress', (progress) => {
|
||||
// logger.debug(progress)
|
||||
// })
|
||||
|
||||
const data = await parallelUploads3.done()
|
||||
// the ETag is a hash of the object. Could be used to dedupe stuff...
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
DeleteBlob,
|
||||
GetBlobMetadata,
|
||||
StoreFileStream,
|
||||
UpdateBlob,
|
||||
UploadFileStream,
|
||||
UpsertBlob
|
||||
@@ -16,8 +17,12 @@ import { MaybeAsync } from '@speckle/shared'
|
||||
export const getFileSizeLimit = () => getFileSizeLimitMB() * 1024 * 1024
|
||||
|
||||
export const uploadFileStreamFactory =
|
||||
(deps: { upsertBlob: UpsertBlob; updateBlob: UpdateBlob }): UploadFileStream =>
|
||||
async (storeFileStream, params1, params2) => {
|
||||
(deps: {
|
||||
upsertBlob: UpsertBlob
|
||||
updateBlob: UpdateBlob
|
||||
storeFileStream: StoreFileStream
|
||||
}): UploadFileStream =>
|
||||
async (params1, params2) => {
|
||||
const { streamId, userId } = params1
|
||||
const { blobId, fileName, fileType, fileStream } = params2
|
||||
|
||||
@@ -39,7 +44,7 @@ export const uploadFileStreamFactory =
|
||||
// even might fire faster, than the db insert, causing missing asset data in the db
|
||||
await deps.upsertBlob(dbFile)
|
||||
|
||||
const { fileHash } = await storeFileStream({ objectKey, fileStream })
|
||||
const { fileHash } = await deps.storeFileStream({ objectKey, fileStream })
|
||||
|
||||
// here we should also update the blob db record with the fileHash
|
||||
await deps.updateBlob({ id: blobId, item: { fileHash } })
|
||||
|
||||
@@ -23,12 +23,14 @@ const { cursorFromRows, decodeCursor } = require('@/modules/blobstorage/helpers/
|
||||
const { createTestStream } = require('@/test/speckle-helpers/streamHelper')
|
||||
const cryptoRandomString = require('crypto-random-string')
|
||||
const { createTestUser } = require('@/test/authHelper')
|
||||
const { storeFileStream } = require('@/modules/blobstorage/objectStorage')
|
||||
const fakeFileStreamStore = (fakeHash) => async () => ({ fileHash: fakeHash })
|
||||
const upsertBlob = upsertBlobFactory({ db })
|
||||
const updateBlob = updateBlobFactory({ db })
|
||||
const uploadFileStream = uploadFileStreamFactory({
|
||||
upsertBlob,
|
||||
updateBlob
|
||||
updateBlob,
|
||||
storeFileStream
|
||||
})
|
||||
const getBlobMetadata = getBlobMetadataFactory({ db })
|
||||
const getBlobMetadataCollection = getBlobMetadataCollectionFactory({ db })
|
||||
@@ -66,7 +68,7 @@ describe('Blob storage @blobstorage', () => {
|
||||
data.map(([caseName, streamData, blobData]) =>
|
||||
it(`Should throw if ${caseName} id length is incorrect`, async () => {
|
||||
try {
|
||||
await uploadFileStream(null, streamData, blobData)
|
||||
await uploadFileStream(streamData, blobData)
|
||||
} catch (err) {
|
||||
if (!(err instanceof BadRequestError)) throw err
|
||||
expect(err.message).to.equal(`The ${caseName} id has to be of length 10`)
|
||||
@@ -81,8 +83,13 @@ describe('Blob storage @blobstorage', () => {
|
||||
const userId = fakeIdGenerator()
|
||||
const fileHash = fakeIdGenerator()
|
||||
|
||||
const uploadFileStream = uploadFileStreamFactory({
|
||||
upsertBlob,
|
||||
updateBlob,
|
||||
storeFileStream: fakeFileStreamStore(fileHash)
|
||||
})
|
||||
|
||||
const blobData = await uploadFileStream(
|
||||
fakeFileStreamStore(fileHash),
|
||||
{ streamId, userId },
|
||||
{ blobId, fileName, fileType: '.something', fileStream: null }
|
||||
)
|
||||
|
||||
@@ -3577,6 +3577,7 @@ export type User = {
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
favoriteStreams: StreamCollection;
|
||||
gendoAICredits: UserGendoAiCredits;
|
||||
/** Whether the user has a pending/active email verification token */
|
||||
hasPendingVerification?: Maybe<Scalars['Boolean']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
@@ -3765,6 +3766,13 @@ export type UserEmailMutationsSetPrimaryArgs = {
|
||||
input: SetPrimaryUserEmailInput;
|
||||
};
|
||||
|
||||
export type UserGendoAiCredits = {
|
||||
__typename?: 'UserGendoAICredits';
|
||||
limit: Scalars['Int']['output'];
|
||||
resetDate: Scalars['DateTime']['output'];
|
||||
used: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type UserProjectsFilter = {
|
||||
/** Only include projects where user has the specified roles */
|
||||
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
@@ -4717,6 +4725,7 @@ export type ResolversTypes = {
|
||||
UserDeleteInput: UserDeleteInput;
|
||||
UserEmail: ResolverTypeWrapper<UserEmail>;
|
||||
UserEmailMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
UserGendoAICredits: ResolverTypeWrapper<UserGendoAiCredits>;
|
||||
UserProjectsFilter: UserProjectsFilter;
|
||||
UserProjectsUpdatedMessage: ResolverTypeWrapper<Omit<UserProjectsUpdatedMessage, 'project'> & { project?: Maybe<ResolversTypes['Project']> }>;
|
||||
UserProjectsUpdatedMessageType: UserProjectsUpdatedMessageType;
|
||||
@@ -4975,6 +4984,7 @@ export type ResolversParentTypes = {
|
||||
UserDeleteInput: UserDeleteInput;
|
||||
UserEmail: UserEmail;
|
||||
UserEmailMutations: MutationsObjectGraphQLReturn;
|
||||
UserGendoAICredits: UserGendoAiCredits;
|
||||
UserProjectsFilter: UserProjectsFilter;
|
||||
UserProjectsUpdatedMessage: Omit<UserProjectsUpdatedMessage, 'project'> & { project?: Maybe<ResolversParentTypes['Project']> };
|
||||
UserRoleInput: UserRoleInput;
|
||||
@@ -6243,6 +6253,7 @@ export type UserResolvers<ContextType = GraphQLContext, ParentType extends Resol
|
||||
emails?: Resolver<Array<ResolversTypes['UserEmail']>, ParentType, ContextType>;
|
||||
expiredSsoSessions?: Resolver<Array<ResolversTypes['LimitedWorkspace']>, ParentType, ContextType>;
|
||||
favoriteStreams?: Resolver<ResolversTypes['StreamCollection'], ParentType, ContextType, RequireFields<UserFavoriteStreamsArgs, 'limit'>>;
|
||||
gendoAICredits?: Resolver<ResolversTypes['UserGendoAICredits'], ParentType, ContextType>;
|
||||
hasPendingVerification?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
isOnboardingFinished?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
@@ -6286,6 +6297,13 @@ export type UserEmailMutationsResolvers<ContextType = GraphQLContext, ParentType
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UserGendoAiCreditsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['UserGendoAICredits'] = ResolversParentTypes['UserGendoAICredits']> = {
|
||||
limit?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
resetDate?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
used?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UserProjectsUpdatedMessageResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['UserProjectsUpdatedMessage'] = ResolversParentTypes['UserProjectsUpdatedMessage']> = {
|
||||
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
project?: Resolver<Maybe<ResolversTypes['Project']>, ParentType, ContextType>;
|
||||
@@ -6653,6 +6671,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
UserAutomateInfo?: UserAutomateInfoResolvers<ContextType>;
|
||||
UserEmail?: UserEmailResolvers<ContextType>;
|
||||
UserEmailMutations?: UserEmailMutationsResolvers<ContextType>;
|
||||
UserGendoAICredits?: UserGendoAiCreditsResolvers<ContextType>;
|
||||
UserProjectsUpdatedMessage?: UserProjectsUpdatedMessageResolvers<ContextType>;
|
||||
UserSearchResultCollection?: UserSearchResultCollectionResolvers<ContextType>;
|
||||
Version?: VersionResolvers<ContextType>;
|
||||
|
||||
@@ -3558,6 +3558,7 @@ export type User = {
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
favoriteStreams: StreamCollection;
|
||||
gendoAICredits: UserGendoAiCredits;
|
||||
/** Whether the user has a pending/active email verification token */
|
||||
hasPendingVerification?: Maybe<Scalars['Boolean']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
@@ -3746,6 +3747,13 @@ export type UserEmailMutationsSetPrimaryArgs = {
|
||||
input: SetPrimaryUserEmailInput;
|
||||
};
|
||||
|
||||
export type UserGendoAiCredits = {
|
||||
__typename?: 'UserGendoAICredits';
|
||||
limit: Scalars['Int']['output'];
|
||||
resetDate: Scalars['DateTime']['output'];
|
||||
used: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type UserProjectsFilter = {
|
||||
/** Only include projects where user has the specified roles */
|
||||
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspace_checkout_sessions', (table) => {
|
||||
table.dropForeign(['workspaceId'])
|
||||
table
|
||||
.foreign('workspaceId')
|
||||
.references('id')
|
||||
.inTable('workspaces')
|
||||
.onDelete('CASCADE')
|
||||
})
|
||||
await knex.schema.alterTable('workspace_subscriptions', (table) => {
|
||||
table.dropForeign(['workspaceId'])
|
||||
table
|
||||
.foreign('workspaceId')
|
||||
.references('id')
|
||||
.inTable('workspaces')
|
||||
.onDelete('CASCADE')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspace_checkout_sessions', (table) => {
|
||||
table.dropForeign(['workspaceId'])
|
||||
table.foreign('workspaceId').references('id').inTable('workspaces')
|
||||
})
|
||||
|
||||
await knex.schema.alterTable('workspace_subscriptions', (table) => {
|
||||
table.dropForeign(['workspaceId'])
|
||||
table.foreign('workspaceId').references('id').inTable('workspaces')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { RequestNewImageGeneration } from '@/modules/gendo/domain/operations'
|
||||
import { GendoRenderRequestError } from '@/modules/gendo/errors/main'
|
||||
|
||||
/**
|
||||
*
|
||||
{
|
||||
"userId": "user1234",
|
||||
"prompt": "a beautiful stone barn next to a lake, sunset, water",
|
||||
"webhookUrl": "<speckle gendo webhook handler endpoint>",
|
||||
"depthMapBase64": "<base64 encoded png depth map>"
|
||||
}
|
||||
*/
|
||||
|
||||
export const requestNewImageGenerationFactory =
|
||||
({
|
||||
endpoint,
|
||||
token,
|
||||
serverOrigin
|
||||
}: {
|
||||
endpoint: string
|
||||
token: string
|
||||
serverOrigin: string
|
||||
}): RequestNewImageGeneration =>
|
||||
async (input) => {
|
||||
const webhookUrl = `${serverOrigin}/api/thirdparty/gendo/${input.projectId}`
|
||||
|
||||
// TODO: Fn handles too many concerns, refactor (e.g. the client fetch call)
|
||||
// TODO: Fire off request to gendo api & get generationId, create record in db. Note: use gendo api key from env
|
||||
const depthMapBase64 = input.baseImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
const gendoRequestBody = {
|
||||
userId: input.userId,
|
||||
depthMapBase64,
|
||||
prompt: input.prompt,
|
||||
webhookUrl
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-token': token
|
||||
},
|
||||
body: JSON.stringify(gendoRequestBody)
|
||||
})
|
||||
|
||||
const status = response.status
|
||||
if (status !== 201) {
|
||||
const body = await response.json().catch((e) => ({ error: `${e}` }))
|
||||
throw new GendoRenderRequestError('Failed to enqueue gendo render.', {
|
||||
info: { body }
|
||||
})
|
||||
}
|
||||
|
||||
const gendoResponseBody = (await response.json()) as {
|
||||
status: string
|
||||
generationId: string
|
||||
}
|
||||
return gendoResponseBody
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GendoAiRenderInput } from '@/modules/core/graph/generated/graphql'
|
||||
import { GendoAIRender } from '@/modules/gendo/domain/types'
|
||||
import { GendoAIRender, UserCredits } from '@/modules/gendo/domain/types'
|
||||
import { NullableKeysToOptional, Optional } from '@speckle/shared'
|
||||
import { SetOptional } from 'type-fest'
|
||||
|
||||
@@ -33,5 +33,16 @@ export type CreateRenderRequest = (
|
||||
|
||||
export type UpdateRenderRequest = (input: {
|
||||
responseImage: string
|
||||
status: string
|
||||
gendoGenerationId: string
|
||||
}) => Promise<GendoAIRender>
|
||||
|
||||
export type RequestNewImageGeneration = (args: {
|
||||
userId: string
|
||||
baseImage: string
|
||||
projectId: string
|
||||
prompt: string
|
||||
}) => Promise<{ status: string; generationId: string }>
|
||||
|
||||
export type GetUserCredits = (args: { userId: string }) => Promise<UserCredits | null>
|
||||
export type UpsertUserCredits = (args: { userCredits: UserCredits }) => Promise<void>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GendoAIRenderRecord } from '@/modules/gendo/helpers/types'
|
||||
|
||||
export type GendoAIRender = GendoAIRenderRecord
|
||||
|
||||
export type UserCredits = { userId: string; used: number; resetDate: Date }
|
||||
|
||||
@@ -11,3 +11,10 @@ export class GendoRenderRequestNotFoundError extends BaseError {
|
||||
static defaultMessage = 'Gendo render request not found'
|
||||
static statusCode = 404
|
||||
}
|
||||
|
||||
export class InsufficientGendoRenderCreditsError extends BaseError {
|
||||
static code = 'INSUFFICIENT_GENDO_RENDER_CREDITS'
|
||||
static defaultMessage =
|
||||
'You do not have enough GendoAi credits left for the operation'
|
||||
static statusCode = 402
|
||||
}
|
||||
|
||||
@@ -20,114 +20,168 @@ import {
|
||||
import { storeFileStream } from '@/modules/blobstorage/objectStorage'
|
||||
import {
|
||||
getLatestVersionRenderRequestsFactory,
|
||||
getUserCreditsFactory,
|
||||
getVersionRenderRequestFactory,
|
||||
storeRenderFactory
|
||||
storeRenderFactory,
|
||||
upsertUserCreditsFactory
|
||||
} from '@/modules/gendo/repositories'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/dbSelector'
|
||||
import { requestNewImageGenerationFactory } from '@/modules/gendo/clients/gendo'
|
||||
import {
|
||||
getUserGendoAiCreditsFactory,
|
||||
useUserGendoAiCreditsFactory
|
||||
} from '@/modules/gendo/services/userCredits'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
getGendoAiApiEndpoint,
|
||||
getGendoAIKey,
|
||||
getGendoAICreditLimit,
|
||||
getServerOrigin,
|
||||
getFeatureFlags
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const createRenderRequest = createRenderRequestFactory({
|
||||
uploadFileStream: uploadFileStreamFactory({
|
||||
upsertBlob: upsertBlobFactory({ db }),
|
||||
updateBlob: updateBlobFactory({ db })
|
||||
}),
|
||||
storeFileStream,
|
||||
storeRender: storeRenderFactory({ db }),
|
||||
publish,
|
||||
fetch
|
||||
const upsertUserCredits = upsertUserCreditsFactory({ db })
|
||||
const getUserGendoAiCredits = getUserGendoAiCreditsFactory({
|
||||
getUserCredits: getUserCreditsFactory({ db }),
|
||||
upsertUserCredits
|
||||
})
|
||||
const getLatestVersionRenderRequests = getLatestVersionRenderRequestsFactory({ db })
|
||||
const getVersionRenderRequest = getVersionRenderRequestFactory({ db })
|
||||
|
||||
export = {
|
||||
Version: {
|
||||
async gendoAIRenders(parent) {
|
||||
const items = await getLatestVersionRenderRequests({ versionId: parent.id })
|
||||
return {
|
||||
totalCount: items.length,
|
||||
items
|
||||
}
|
||||
},
|
||||
async gendoAIRender(parent, args) {
|
||||
const item = await getVersionRenderRequest({
|
||||
versionId: parent.id,
|
||||
id: args.id
|
||||
})
|
||||
const { FF_GENDOAI_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
return item
|
||||
}
|
||||
},
|
||||
GendoAIRender: {
|
||||
async user(parent, __args, ctx) {
|
||||
return await ctx.loaders.users.getUser.load(parent.userId)
|
||||
}
|
||||
},
|
||||
VersionMutations: {
|
||||
async requestGendoAIRender(__parent, args, ctx) {
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
const rateLimitResult = await getRateLimitResult(
|
||||
'GENDO_AI_RENDER_REQUEST',
|
||||
ctx.userId as string
|
||||
)
|
||||
if (isRateLimitBreached(rateLimitResult)) {
|
||||
throw new RateLimitError(rateLimitResult)
|
||||
}
|
||||
|
||||
await createRenderRequest({
|
||||
...args.input,
|
||||
userId: ctx.userId!
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
Subscription: {
|
||||
projectVersionGendoAIRenderCreated: {
|
||||
subscribe: filteredSubscribe(
|
||||
ProjectSubscriptions.ProjectVersionGendoAIRenderCreated,
|
||||
async (payload, args, ctx) => {
|
||||
if (
|
||||
args.id !== payload.projectVersionGendoAIRenderCreated.projectId ||
|
||||
args.versionId !== payload.projectVersionGendoAIRenderCreated.versionId
|
||||
)
|
||||
return false
|
||||
export = FF_GENDOAI_MODULE_ENABLED
|
||||
? ({
|
||||
Version: {
|
||||
async gendoAIRenders(parent) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
||||
const items = await getLatestVersionRenderRequestsFactory({ db: projectDb })({
|
||||
versionId: parent.id
|
||||
})
|
||||
return {
|
||||
totalCount: items.length,
|
||||
items
|
||||
}
|
||||
},
|
||||
async gendoAIRender(parent, args) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
||||
const item = await getVersionRenderRequestFactory({ db: projectDb })({
|
||||
versionId: parent.id,
|
||||
id: args.id
|
||||
})
|
||||
|
||||
return item
|
||||
}
|
||||
},
|
||||
GendoAIRender: {
|
||||
async user(parent, __args, ctx) {
|
||||
return await ctx.loaders.users.getUser.load(parent.userId)
|
||||
}
|
||||
},
|
||||
VersionMutations: {
|
||||
async requestGendoAIRender(__parent, args, ctx) {
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.id,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
},
|
||||
projectVersionGendoAIRenderUpdated: {
|
||||
subscribe: filteredSubscribe(
|
||||
ProjectSubscriptions.ProjectVersionGendoAIRenderUpdated,
|
||||
async (payload, args, ctx) => {
|
||||
if (
|
||||
args.id !== payload.projectVersionGendoAIRenderUpdated.projectId ||
|
||||
args.versionId !== payload.projectVersionGendoAIRenderUpdated.versionId
|
||||
const rateLimitResult = await getRateLimitResult(
|
||||
'GENDO_AI_RENDER_REQUEST',
|
||||
ctx.userId as string
|
||||
)
|
||||
return false
|
||||
if (isRateLimitBreached(rateLimitResult)) {
|
||||
throw new RateLimitError(rateLimitResult)
|
||||
}
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.id,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const userId = ctx.userId!
|
||||
|
||||
const projectDb = await getProjectDbClient({
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
|
||||
await useUserGendoAiCreditsFactory({
|
||||
getUserGendoAiCredits,
|
||||
upsertUserCredits,
|
||||
maxCredits: getGendoAICreditLimit()
|
||||
})({ userId, credits: 1 })
|
||||
|
||||
const requestNewImageGeneration = requestNewImageGenerationFactory({
|
||||
endpoint: getGendoAiApiEndpoint(),
|
||||
serverOrigin: getServerOrigin(),
|
||||
token: getGendoAIKey()
|
||||
})
|
||||
|
||||
const createRenderRequest = createRenderRequestFactory({
|
||||
uploadFileStream: uploadFileStreamFactory({
|
||||
storeFileStream,
|
||||
upsertBlob: upsertBlobFactory({ db: projectDb }),
|
||||
updateBlob: updateBlobFactory({ db: projectDb })
|
||||
}),
|
||||
requestNewImageGeneration,
|
||||
storeRender: storeRenderFactory({ db: projectDb }),
|
||||
publish
|
||||
})
|
||||
|
||||
await createRenderRequest({
|
||||
...args.input,
|
||||
userId
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} as Resolvers
|
||||
},
|
||||
User: {
|
||||
gendoAICredits: async (_parent, _args, ctx) => {
|
||||
const userCredits = await getUserGendoAiCredits({ userId: ctx.userId! })
|
||||
return {
|
||||
limit: getGendoAICreditLimit(),
|
||||
...userCredits
|
||||
}
|
||||
}
|
||||
},
|
||||
Subscription: {
|
||||
projectVersionGendoAIRenderCreated: {
|
||||
subscribe: filteredSubscribe(
|
||||
ProjectSubscriptions.ProjectVersionGendoAIRenderCreated,
|
||||
async (payload, args, ctx) => {
|
||||
if (
|
||||
args.id !== payload.projectVersionGendoAIRenderCreated.projectId ||
|
||||
args.versionId !== payload.projectVersionGendoAIRenderCreated.versionId
|
||||
)
|
||||
return false
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.id,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
},
|
||||
projectVersionGendoAIRenderUpdated: {
|
||||
subscribe: filteredSubscribe(
|
||||
ProjectSubscriptions.ProjectVersionGendoAIRenderUpdated,
|
||||
async (payload, args, ctx) => {
|
||||
if (
|
||||
args.id !== payload.projectVersionGendoAIRenderUpdated.projectId ||
|
||||
args.versionId !== payload.projectVersionGendoAIRenderUpdated.versionId
|
||||
)
|
||||
return false
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.id,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} as Resolvers)
|
||||
: {}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('gendo_user_credits', (table) => {
|
||||
table
|
||||
.string('userId')
|
||||
.primary()
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('cascade')
|
||||
table.timestamp('resetDate', { precision: 3, useTz: true }).notNullable()
|
||||
table.integer('used').notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable('gendo_user_credits')
|
||||
}
|
||||
@@ -2,16 +2,20 @@ import { GendoAIRenders } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
GetLatestVersionRenderRequests,
|
||||
GetRenderByGenerationId,
|
||||
GetUserCredits,
|
||||
GetVersionRenderRequest,
|
||||
StoreRender,
|
||||
UpdateRenderRecord
|
||||
UpdateRenderRecord,
|
||||
UpsertUserCredits
|
||||
} from '@/modules/gendo/domain/operations'
|
||||
import { UserCredits } from '@/modules/gendo/domain/types'
|
||||
import { GendoAIRenderRecord } from '@/modules/gendo/helpers/types'
|
||||
import { Knex } from 'knex'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
const tables = {
|
||||
gendoAIRenders: (db: Knex) => db<GendoAIRenderRecord>(GendoAIRenders.name)
|
||||
gendoAIRenders: (db: Knex) => db<GendoAIRenderRecord>(GendoAIRenders.name),
|
||||
gendoUserCredits: (db: Knex) => db<UserCredits>('gendo_user_credits')
|
||||
}
|
||||
|
||||
export const storeRenderFactory =
|
||||
@@ -68,3 +72,21 @@ export const getVersionRenderRequestFactory =
|
||||
.first()
|
||||
return record
|
||||
}
|
||||
|
||||
export const getUserCreditsFactory =
|
||||
({ db }: { db: Knex }): GetUserCredits =>
|
||||
async ({ userId }) => {
|
||||
const userCredits = await tables
|
||||
.gendoUserCredits(db)
|
||||
.select()
|
||||
.where({ userId })
|
||||
.first()
|
||||
|
||||
return userCredits || null
|
||||
}
|
||||
|
||||
export const upsertUserCreditsFactory =
|
||||
({ db }: { db: Knex }): UpsertUserCredits =>
|
||||
async ({ userCredits }) => {
|
||||
await tables.gendoUserCredits(db).insert(userCredits).onConflict('userId').merge()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { corsMiddleware } from '@/modules/core/configs/cors'
|
||||
import { getGendoAIResponseKey } from '@/modules/shared/helpers/envHelper'
|
||||
import { updateRenderRequestFactory } from '@/modules/gendo/services'
|
||||
import type express from 'express'
|
||||
import {
|
||||
getRenderByGenerationIdFactory,
|
||||
updateRenderRecordFactory
|
||||
} from '@/modules/gendo/repositories'
|
||||
import { db } from '@/db/knex'
|
||||
import { uploadFileStreamFactory } from '@/modules/blobstorage/services/management'
|
||||
import { storeFileStream } from '@/modules/blobstorage/objectStorage'
|
||||
import {
|
||||
@@ -14,34 +12,57 @@ import {
|
||||
upsertBlobFactory
|
||||
} from '@/modules/blobstorage/repositories'
|
||||
import { publish } from '@/modules/shared/utils/subscriptions'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/dbSelector'
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto'
|
||||
import { getGendoAIKey } from '@/modules/shared/helpers/envHelper'
|
||||
//Validate payload
|
||||
// function validatePayload(req, res, next) {
|
||||
// if (req.get(sigHeaderName)) {
|
||||
// //Extract Signature header
|
||||
//
|
||||
// }
|
||||
|
||||
// return next();
|
||||
// }
|
||||
|
||||
export default function (app: express.Express) {
|
||||
const responseToken = getGendoAIResponseKey()
|
||||
const updateRenderRequest = updateRenderRequestFactory({
|
||||
getRenderByGenerationId: getRenderByGenerationIdFactory({ db }),
|
||||
uploadFileStream: uploadFileStreamFactory({
|
||||
upsertBlob: upsertBlobFactory({ db }),
|
||||
updateBlob: updateBlobFactory({ db })
|
||||
}),
|
||||
storeFileStream,
|
||||
updateRenderRecord: updateRenderRecordFactory({ db }),
|
||||
publish
|
||||
})
|
||||
// const responseToken = getGendoAIResponseKey()
|
||||
|
||||
// Gendo api calls hit these endpoints w/ the results
|
||||
app.options('/api/thirdparty/gendo', corsMiddleware())
|
||||
app.post('/api/thirdparty/gendo', corsMiddleware(), async (req, res) => {
|
||||
if (req.headers['x-gendo-authorization'] !== responseToken) {
|
||||
return res.status(401).send('Speckle says you are not authorized 😠')
|
||||
}
|
||||
app.options('/api/thirdparty/gendo/:projectId', corsMiddleware())
|
||||
app.post('/api/thirdparty/gendo/:projectId', corsMiddleware(), async (req, res) => {
|
||||
const sig = Buffer.from(req.get('x-signature-sha256') || '', 'utf8')
|
||||
|
||||
const responseImage = req.body.generated_image
|
||||
// const status = req.body.status
|
||||
const gendoGenerationId = req.body.generationId
|
||||
// //Calculate HMAC
|
||||
const hmac = createHmac('sha256', getGendoAIKey())
|
||||
const digest = Buffer.from(hmac.update(req.body).digest('base64'), 'utf-8')
|
||||
|
||||
// //Compare HMACs
|
||||
if (sig.length !== digest.length || !timingSafeEqual(digest, sig)) {
|
||||
return res.status(401).send('Speckle says your webhook signature is not valid 😠')
|
||||
}
|
||||
const payload = JSON.parse(req.body)
|
||||
const responseImage = payload.imageBase64
|
||||
const status = payload.status
|
||||
const gendoGenerationId = payload.generationId
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: req.params.projectId })
|
||||
|
||||
const updateRenderRequest = updateRenderRequestFactory({
|
||||
getRenderByGenerationId: getRenderByGenerationIdFactory({ db: projectDb }),
|
||||
uploadFileStream: uploadFileStreamFactory({
|
||||
storeFileStream,
|
||||
upsertBlob: upsertBlobFactory({ db: projectDb }),
|
||||
updateBlob: updateBlobFactory({ db: projectDb })
|
||||
}),
|
||||
updateRenderRecord: updateRenderRecordFactory({ db: projectDb }),
|
||||
publish
|
||||
})
|
||||
|
||||
await updateRenderRequest({
|
||||
gendoGenerationId,
|
||||
responseImage
|
||||
responseImage,
|
||||
status
|
||||
})
|
||||
|
||||
res.status(200).send('Speckle says thank you 💖')
|
||||
|
||||
@@ -3,69 +3,33 @@ import {
|
||||
ProjectSubscriptions,
|
||||
PublishSubscription
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import { storeFileStream } from '@/modules/blobstorage/objectStorage'
|
||||
|
||||
import {
|
||||
CreateRenderRequest,
|
||||
GetRenderByGenerationId,
|
||||
RequestNewImageGeneration,
|
||||
StoreRender,
|
||||
UpdateRenderRecord,
|
||||
UpdateRenderRequest
|
||||
} from '@/modules/gendo/domain/operations'
|
||||
import { UploadFileStream } from '@/modules/blobstorage/domain/operations'
|
||||
import {
|
||||
getGendoAIAPIEndpoint,
|
||||
getGendoAIKey,
|
||||
getServerOrigin
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
GendoRenderRequestError,
|
||||
GendoRenderRequestNotFoundError
|
||||
} from '@/modules/gendo/errors/main'
|
||||
import { GendoRenderRequestNotFoundError } from '@/modules/gendo/errors/main'
|
||||
|
||||
export const createRenderRequestFactory =
|
||||
(deps: {
|
||||
requestNewImageGeneration: RequestNewImageGeneration
|
||||
uploadFileStream: UploadFileStream
|
||||
storeFileStream: typeof storeFileStream
|
||||
storeRender: StoreRender
|
||||
publish: PublishSubscription
|
||||
fetch: typeof fetch
|
||||
}): CreateRenderRequest =>
|
||||
async (input) => {
|
||||
const endpoint = getGendoAIAPIEndpoint()
|
||||
const bearer = getGendoAIKey() as string
|
||||
const webhookUrl = `${getServerOrigin()}/api/thirdparty/gendo`
|
||||
|
||||
// TODO: Fn handles too many concerns, refactor (e.g. the client fetch call)
|
||||
// TODO: Fire off request to gendo api & get generationId, create record in db. Note: use gendo api key from env
|
||||
const gendoRequestBody = {
|
||||
const imageRequest = await deps.requestNewImageGeneration({
|
||||
userId: input.userId,
|
||||
depthMap: input.baseImage,
|
||||
baseImage: input.baseImage,
|
||||
prompt: input.prompt,
|
||||
webhookUrl
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${bearer}`
|
||||
},
|
||||
body: JSON.stringify(gendoRequestBody)
|
||||
projectId: input.projectId
|
||||
})
|
||||
|
||||
const status = response.status
|
||||
if (status !== 200) {
|
||||
const body = await response.json().catch((e) => ({ error: `${e}` }))
|
||||
throw new GendoRenderRequestError('Failed to enqueue gendo render.', {
|
||||
info: { body }
|
||||
})
|
||||
}
|
||||
|
||||
const gendoResponseBody = (await response.json()) as {
|
||||
status: string
|
||||
generationId: string
|
||||
}
|
||||
// ----
|
||||
const baseImageBuffer = Buffer.from(
|
||||
input.baseImage.replace(/^data:image\/\w+;base64,/, ''),
|
||||
'base64'
|
||||
@@ -73,7 +37,6 @@ export const createRenderRequestFactory =
|
||||
|
||||
const blobId = crs({ length: 10 })
|
||||
await deps.uploadFileStream(
|
||||
deps.storeFileStream,
|
||||
{ streamId: input.projectId, userId: input.userId },
|
||||
{
|
||||
blobId,
|
||||
@@ -87,8 +50,8 @@ export const createRenderRequestFactory =
|
||||
|
||||
const newRecord = await deps.storeRender({
|
||||
...input,
|
||||
status: gendoResponseBody.status,
|
||||
gendoGenerationId: gendoResponseBody.generationId,
|
||||
status: imageRequest.status,
|
||||
gendoGenerationId: imageRequest.generationId,
|
||||
id: crs({ length: 10 })
|
||||
})
|
||||
|
||||
@@ -105,7 +68,6 @@ export const updateRenderRequestFactory =
|
||||
(deps: {
|
||||
getRenderByGenerationId: GetRenderByGenerationId
|
||||
uploadFileStream: UploadFileStream
|
||||
storeFileStream: typeof storeFileStream
|
||||
updateRenderRecord: UpdateRenderRecord
|
||||
publish: PublishSubscription
|
||||
}): UpdateRenderRequest =>
|
||||
@@ -131,7 +93,6 @@ export const updateRenderRequestFactory =
|
||||
|
||||
const blobId = crs({ length: 10 })
|
||||
await deps.uploadFileStream(
|
||||
deps.storeFileStream,
|
||||
{ streamId: baseRequest.projectId, userId: baseRequest.userId },
|
||||
{
|
||||
blobId,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { GetUserCredits, UpsertUserCredits } from '@/modules/gendo/domain/operations'
|
||||
import { UserCredits } from '@/modules/gendo/domain/types'
|
||||
import { InsufficientGendoRenderCreditsError } from '@/modules/gendo/errors/main'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
type GetUserGendoAiCredits = (args: { userId: string }) => Promise<UserCredits>
|
||||
|
||||
export const getUserGendoAiCreditsFactory =
|
||||
({
|
||||
getUserCredits,
|
||||
upsertUserCredits
|
||||
}: {
|
||||
getUserCredits: GetUserCredits
|
||||
upsertUserCredits: UpsertUserCredits
|
||||
}) =>
|
||||
async ({ userId }: { userId: string }) => {
|
||||
//
|
||||
const userCredits = await getUserCredits({ userId })
|
||||
if (userCredits && userCredits.resetDate.getTime() > new Date().getTime())
|
||||
return userCredits
|
||||
|
||||
const resetDate = dayjs(userCredits?.resetDate || new Date())
|
||||
.add(1, 'month')
|
||||
.toDate()
|
||||
|
||||
const newCredits = {
|
||||
used: 0,
|
||||
userId,
|
||||
resetDate
|
||||
}
|
||||
await upsertUserCredits({ userCredits: newCredits })
|
||||
return newCredits
|
||||
}
|
||||
|
||||
export const useUserGendoAiCreditsFactory =
|
||||
({
|
||||
getUserGendoAiCredits,
|
||||
upsertUserCredits,
|
||||
maxCredits
|
||||
}: {
|
||||
getUserGendoAiCredits: GetUserGendoAiCredits
|
||||
upsertUserCredits: UpsertUserCredits
|
||||
maxCredits: number
|
||||
}) =>
|
||||
async ({ userId, credits }: { userId: string; credits: number }) => {
|
||||
const userCredits = await getUserGendoAiCredits({ userId })
|
||||
userCredits.used += credits
|
||||
if (userCredits.used > maxCredits) throw new InsufficientGendoRenderCreditsError()
|
||||
await upsertUserCredits({ userCredits })
|
||||
}
|
||||
@@ -350,11 +350,11 @@ export function getGendoAIKey() {
|
||||
return getStringFromEnv('GENDOAI_KEY')
|
||||
}
|
||||
|
||||
export function getGendoAIResponseKey() {
|
||||
return getStringFromEnv('GENDOAI_KEY_RESPONSE')
|
||||
export function getGendoAICreditLimit() {
|
||||
return getIntFromEnv('GENDOAI_CREDIT_LIMIT')
|
||||
}
|
||||
|
||||
export function getGendoAIAPIEndpoint() {
|
||||
export function getGendoAiApiEndpoint() {
|
||||
return getStringFromEnv('GENDOAI_API_ENDPOINT')
|
||||
}
|
||||
|
||||
|
||||
@@ -3559,6 +3559,7 @@ export type User = {
|
||||
* @deprecated Part of the old API surface and will be removed in the future.
|
||||
*/
|
||||
favoriteStreams: StreamCollection;
|
||||
gendoAICredits: UserGendoAiCredits;
|
||||
/** Whether the user has a pending/active email verification token */
|
||||
hasPendingVerification?: Maybe<Scalars['Boolean']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
@@ -3747,6 +3748,13 @@ export type UserEmailMutationsSetPrimaryArgs = {
|
||||
input: SetPrimaryUserEmailInput;
|
||||
};
|
||||
|
||||
export type UserGendoAiCredits = {
|
||||
__typename?: 'UserGendoAICredits';
|
||||
limit: Scalars['Int']['output'];
|
||||
resetDate: Scalars['DateTime']['output'];
|
||||
used: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type UserProjectsFilter = {
|
||||
/** Only include projects where user has the specified roles */
|
||||
onlyWithRoles?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
|
||||
@@ -731,15 +731,12 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
name: {{ default .Values.secretName .Values.server.gendoAI.key.secretName }}
|
||||
key: {{ .Values.server.gendoAI.key.secretKey }}
|
||||
|
||||
- name: GENDOAI_KEY_RESPONSE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default .Values.secretName .Values.server.gendoAI.keyResponse.secretName }}
|
||||
key: {{ .Values.server.gendoAI.keyResponse.secretKey }}
|
||||
|
||||
- name: GENDOAI_API_ENDPOINT
|
||||
value: {{ .Values.server.gendoAI.apiUrl | quote }}
|
||||
|
||||
- name: GENDOAI_CREDIT_LIMIT
|
||||
value: {{ .Values.server.gendoAI.creditLimit | quote }}
|
||||
|
||||
- name: RATELIMIT_GENDO_AI_RENDER_REQUEST
|
||||
value: {{ .Values.server.gendoai.ratelimiting.renderRequest | quote }}
|
||||
|
||||
|
||||
@@ -644,6 +644,11 @@
|
||||
"description": "The url of the Gendo AI application, including protocol.",
|
||||
"default": "https://api.gendo.ai/external/generate"
|
||||
},
|
||||
"creditLimit": {
|
||||
"type": "number",
|
||||
"description": "The number of Gendo AI credit a user gets every month.",
|
||||
"default": 40
|
||||
},
|
||||
"key": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -659,21 +664,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretName": {
|
||||
"type": "string",
|
||||
"description": "The name of the Kubernetes Secret containing the Gendo AI key response. If left blank, will default to the `secretName` parameter.",
|
||||
"default": ""
|
||||
},
|
||||
"secretKey": {
|
||||
"type": "string",
|
||||
"description": "The key within the Kubernetes Secret holding the Gendo AI key response as its value.",
|
||||
"default": "gendoai_key_response"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ratelimiting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -467,16 +467,14 @@ server:
|
||||
gendoAI:
|
||||
## @param server.gendoAI.apiUrl The url of the Gendo AI application, including protocol.
|
||||
apiUrl: 'https://api.gendo.ai/external/generate'
|
||||
|
||||
## @param server.gendoAI.creditLimit The number of Gendo AI credit a user gets every month.
|
||||
creditLimit: 40
|
||||
key:
|
||||
## @param server.gendoAI.key.secretName The name of the Kubernetes Secret containing the Gendo AI key. If left blank, will default to the `secretName` parameter.
|
||||
secretName: ''
|
||||
## @param server.gendoAI.key.secretKey The key within the Kubernetes Secret holding the Gendo AI key as its value.
|
||||
secretKey: 'gendoai_key'
|
||||
keyResponse:
|
||||
## @param server.gendoAI.keyResponse.secretName The name of the Kubernetes Secret containing the Gendo AI key response. If left blank, will default to the `secretName` parameter.
|
||||
secretName: ''
|
||||
## @param server.gendoAI.keyResponse.secretKey The key within the Kubernetes Secret holding the Gendo AI key response as its value.
|
||||
secretKey: 'gendoai_key_response'
|
||||
ratelimiting:
|
||||
## @param server.gendoAI.ratelimiting.renderRequest The number of render requests allowed per period
|
||||
renderRequest: 1
|
||||
|
||||
@@ -95,6 +95,8 @@
|
||||
"Bursty",
|
||||
"discoverability",
|
||||
"Encryptor",
|
||||
"Gendo",
|
||||
"GENDOAI",
|
||||
"Insertable",
|
||||
"mjml",
|
||||
"OIDC",
|
||||
|
||||
Reference in New Issue
Block a user