From 6182c72e783ff83261d9d84c23b9d588408f3c8b Mon Sep 17 00:00:00 2001 From: edmonday Date: Tue, 17 Mar 2026 23:32:31 +0000 Subject: [PATCH 01/10] feat(journeys): add AI chat journey editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New dedicated AI editor at /journeys/[id]/ai with chat interface for plain-language journey editing. Backend journeyAiEdit mutation uses Gemini 2.5 Flash with discriminated union schema to return either a proposed journey update or a text-only suggestion. Frontend includes AiChat panel, AiEditorFlowMap for card navigation, AiEditorCardPreview, and entry points in the journey card menu and editor toolbar. 🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.com/claude-code) + Compound Engineering v2.42.0 Co-Authored-By: Claude Sonnet 4.6 (200K context) --- .../src/schema/journeyAiEdit/index.ts | 1 + .../src/schema/journeyAiEdit/journeyAiEdit.ts | 184 +++++++ .../src/schema/journeyAiEdit/prompts.ts | 74 +++ apis/api-journeys-modern/src/schema/schema.ts | 1 + .../pages/journeys/[journeyId]/ai.tsx | 188 ++++++++ .../src/components/AiEditor/AiChat/AiChat.tsx | 450 ++++++++++++++++++ .../AiEditor/AiChat/AiChatInput.tsx | 95 ++++ .../AiEditor/AiChat/AiChatMessage.tsx | 175 +++++++ .../AiEditor/AiEditorCardPreview.tsx | 230 +++++++++ .../components/AiEditor/AiEditorFlowMap.tsx | 235 +++++++++ .../components/AiEditor/AiEditorHeader.tsx | 87 ++++ .../components/AiEditor/AiEditorPreview.tsx | 57 +++ .../src/components/Editor/Toolbar/Toolbar.tsx | 12 + .../DefaultMenu/DefaultMenu.tsx | 9 + 14 files changed, 1798 insertions(+) create mode 100644 apis/api-journeys-modern/src/schema/journeyAiEdit/index.ts create mode 100644 apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts create mode 100644 apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts create mode 100644 apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiChat/AiChatMessage.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiEditorHeader.tsx create mode 100644 apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/index.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/index.ts new file mode 100644 index 00000000000..141b0da7aeb --- /dev/null +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/index.ts @@ -0,0 +1 @@ +import './journeyAiEdit' diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts new file mode 100644 index 00000000000..114f066c217 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -0,0 +1,184 @@ +import { google } from '@ai-sdk/google' +import { generateObject, ModelMessage, NoObjectGeneratedError } from 'ai' +import { GraphQLError } from 'graphql' +import { z } from 'zod' + +import { prisma } from '@core/prisma/journeys/client' +import { hardenPrompt } from '@core/shared/ai/prompts' +import { + JourneySimple, + journeySimpleSchemaUpdate +} from '@core/shared/ai/journeySimpleTypes' + +import { Action, ability, subject } from '../../lib/auth/ability' +import { builder } from '../builder' +import { getSimpleJourney } from '../journey/simple/getSimpleJourney' + +import { buildSystemPrompt } from './prompts' + +// Return type for the mutation +interface JourneyAiEditResult { + reply: string + proposedJourney: JourneySimple | null +} + +const JourneyAiEditResultRef = + builder.objectRef('JourneyAiEditResult') + +builder.objectType(JourneyAiEditResultRef, { + fields: (t) => ({ + reply: t.string({ + resolve: (parent) => parent.reply + }), + proposedJourney: t.field({ + type: 'Json', + nullable: true, + resolve: (parent) => parent.proposedJourney + }) + }) +}) + +// Input type +const JourneyAiEditInput = builder.inputType('JourneyAiEditInput', { + fields: (t) => ({ + journeyId: t.id({ required: true }), + message: t.string({ required: true }), + history: t.field({ type: 'Json', required: false }), + selectedCardId: t.string({ required: false }) + }) +}) + +// Discriminated union schema for AI response +const journeyAiEditSchema = z.discriminatedUnion('hasChanges', [ + z.object({ + hasChanges: z.literal(true), + reply: z + .string() + .describe('Plain language explanation of what was changed and why'), + journey: journeySimpleSchemaUpdate.describe( + 'Full updated journey with all changes applied' + ) + }), + z.object({ + hasChanges: z.literal(false), + reply: z.string().describe('Plain language suggestions or answer') + }) +]) + +builder.mutationField('journeyAiEdit', (t) => + t.withAuth({ isAuthenticated: true }).field({ + type: JourneyAiEditResultRef, + nullable: false, + args: { + input: t.arg({ + type: JourneyAiEditInput, + required: true + }) + }, + resolve: async (_parent, { input }, context) => { + // 1. Validate message length + if (input.message.length > 2000) { + throw new GraphQLError( + 'Message exceeds maximum length of 2000 characters', + { + extensions: { code: 'BAD_USER_INPUT' } + } + ) + } + + // 2. Fetch journey and validate ACL + const dbJourney = await prisma.journey.findUnique({ + where: { id: input.journeyId }, + include: { + userJourneys: true, + team: { include: { userTeams: true } } + } + }) + + if (!dbJourney) { + throw new GraphQLError('journey not found', { + extensions: { code: 'NOT_FOUND' } + }) + } + + if (!ability(Action.Update, subject('Journey', dbJourney), context.user)) { + throw new GraphQLError( + 'user does not have permission to update journey', + { extensions: { code: 'FORBIDDEN' } } + ) + } + + // 3. Fetch simple journey representation + const currentJourney = await getSimpleJourney(input.journeyId) + + // 4. Prune history to last 10 turns + const rawHistory = + (input.history as ModelMessage[] | null | undefined) ?? [] + const prunedHistory = rawHistory.slice(-10) + + // 5. Harden user message + const hardenedMessage = hardenPrompt(input.message) + + // 6. Build system prompt + const systemPrompt = buildSystemPrompt( + currentJourney, + input.selectedCardId ?? undefined + ) + + // 7. Call generateObject + let aiResult: z.infer + try { + const { object } = await generateObject({ + model: google('gemini-2.5-flash-preview-04-17'), + system: systemPrompt, + messages: [ + ...prunedHistory, + { role: 'user', content: hardenedMessage } + ] as ModelMessage[], + schema: journeyAiEditSchema, + maxRetries: 2, + abortSignal: AbortSignal.timeout(30_000) + }) + aiResult = object + } catch (error) { + if (error instanceof NoObjectGeneratedError) { + console.error('journeyAiEdit: NoObjectGeneratedError', { + journeyId: input.journeyId, + rawOutput: error.text + }) + return { + reply: + 'Something went wrong generating a response. Please try rephrasing your request.', + proposedJourney: null + } + } + console.error('journeyAiEdit: generateObject error', error) + return { + reply: 'Something went wrong. Please try again.', + proposedJourney: null + } + } + + // 8. Audit log + console.log('journeyAiEdit audit', { + userId: context.user.id, + journeyId: input.journeyId, + timestamp: new Date().toISOString(), + hadProposal: aiResult.hasChanges + }) + + // 9. Return result + if (aiResult.hasChanges) { + return { + reply: aiResult.reply, + proposedJourney: aiResult.journey as JourneySimple + } + } + + return { + reply: aiResult.reply, + proposedJourney: null + } + } + }) +) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts new file mode 100644 index 00000000000..03c2babb7f7 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts @@ -0,0 +1,74 @@ +import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' + +export function buildSystemPrompt( + journey: JourneySimple, + selectedCardId?: string +): string { + const screenCount = journey.cards.length + + // Find selected card if provided + const selectedCard = selectedCardId + ? journey.cards.find((c) => c.id === selectedCardId) + : undefined + const selectedIndex = selectedCard + ? journey.cards.indexOf(selectedCard) + 1 + : undefined + + let prompt = `You are an AI assistant that helps users edit journey content. + +IMPORTANT LANGUAGE RULES: +- NEVER use technical terms like "block", "step", "card", "StepBlock", "CardBlock", or any internal type names +- Say "screen" or "slide" when referring to a section of the journey +- Say "intro screen", "first screen", "last screen", etc. for positions +- Describe changes in plain language: "I've updated your intro text" not "I modified card-1" +- Reply in the same language the user writes in + +OUTPUT CONTRACT: +- When making changes, set hasChanges: true and return the COMPLETE updated journey JSON in the journey field +- When only giving advice or answering questions, set hasChanges: false (no journey field needed) +- Always include a clear, plain-language explanation in the reply field + +CURRENT JOURNEY STATE: +Title: ${journey.title} +Description: ${journey.description} +Screens: ${screenCount} ${screenCount === 1 ? 'screen' : 'screens'} + +Full journey JSON (for your internal use — never mention this structure to users): +${JSON.stringify(journey, null, 2)} +` + + if (selectedCard !== undefined && selectedIndex !== undefined) { + const heading = selectedCard.heading ?? '(no heading)' + prompt += ` +SELECTED SCREEN CONTEXT: +The user is currently viewing screen ${selectedIndex}: "${heading}". +References to "this screen", "here", "the current one", or "this slide" mean this specific screen. +` + } + + prompt += ` +CONTENT TYPES AVAILABLE: +- Title/heading text +- Body text +- Image (displayed on the screen) +- Background image (fills the entire screen background) +- Video (YouTube URL — if a screen has a video, it can only contain a video and a next-screen link) +- Button (navigates to next screen or external URL) +- Multiple-choice poll (each option navigates to a specific screen or URL) + +CONSTRAINTS: +- A screen with a video can ONLY have a video and a link to the next screen — no heading, text, buttons, or polls +- Every screen must have a way to go to the next screen (button, poll options, video's defaultNextCard, or defaultNextCard) +- When returning a full updated journey, include ALL screens — not just the changed ones +- Journey cards must always have valid navigation so users can always progress + +BEHAVIORAL RULES: +- Act on additive and editing requests immediately +- Ask for confirmation before bulk deletions (e.g., "delete all screens") +- If a journey is empty (no screens), offer to add an introduction screen +- Describe what you changed in plain terms +- Do not mention internal IDs or field names to users +` + + return prompt +} diff --git a/apis/api-journeys-modern/src/schema/schema.ts b/apis/api-journeys-modern/src/schema/schema.ts index 509d23220cc..5259b5cb3fa 100644 --- a/apis/api-journeys-modern/src/schema/schema.ts +++ b/apis/api-journeys-modern/src/schema/schema.ts @@ -10,6 +10,7 @@ import './host' import './googleSheetsSync' import './integration' import './journey' +import './journeyAiEdit' import './journeyAiTranslate' import './journeyCollection' import './journeyEventsExportLog' diff --git a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx new file mode 100644 index 00000000000..587c9e73465 --- /dev/null +++ b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx @@ -0,0 +1,188 @@ +import { gql, useQuery } from '@apollo/client' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' +import Typography from '@mui/material/Typography' +import { GetServerSidePropsContext } from 'next' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' +import { NextSeo } from 'next-seo' +import { ReactElement, useCallback, useState } from 'react' + +import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' + +import { AiChat, AiState } from '../../../src/components/AiEditor/AiChat/AiChat' +import { AiEditorHeader } from '../../../src/components/AiEditor/AiEditorHeader' +import { AiEditorPreview } from '../../../src/components/AiEditor/AiEditorPreview' +import { + getAuthTokens, + redirectToLogin, + toUser +} from '../../../src/libs/auth/getAuthTokens' +import { initAndAuthApp } from '../../../src/libs/initAndAuthApp' + +const GET_AI_EDITOR_JOURNEY = gql` + query GetAiEditorJourney($id: ID!) { + journey: adminJourney(id: $id, idType: databaseId) { + id + title + description + } + } +` + +function AiEditorPage(): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const router = useRouter() + const journeyId = router.query.journeyId as string + + const [currentJourney, setCurrentJourney] = useState( + null + ) + const [aiState, setAiState] = useState({ + status: 'idle', + affectedCardIds: [] + }) + const [selectedCardId, setSelectedCardId] = useState(null) + + const { data, loading } = useQuery(GET_AI_EDITOR_JOURNEY, { + variables: { id: journeyId }, + skip: journeyId == null + }) + + const journey = data?.journey + + const selectedCardIndex = + currentJourney != null && selectedCardId != null + ? currentJourney.cards.findIndex((c) => c.id === selectedCardId) + 1 + : null + + const handleJourneyUpdated = useCallback((updatedJourney: JourneySimple) => { + setCurrentJourney(updatedJourney) + setAiState({ status: 'idle', affectedCardIds: [] }) + }, []) + + const handleSelectedCardChange = useCallback( + (cardId: string | null) => { + setSelectedCardId(cardId) + }, + [] + ) + + const handleClearSelectedCard = useCallback(() => { + setSelectedCardId(null) + }, []) + + if (loading || journeyId == null) { + return ( + + + + ) + } + + return ( + <> + + + + + 0 + ? selectedCardIndex + : null + } + onClearSelectedCard={handleClearSelectedCard} + onAiState={setAiState} + onJourneyUpdated={handleJourneyUpdated} + sx={{ + width: { xs: '100%', md: '40%' }, + display: { + xs: selectedCardId != null ? 'none' : 'flex', + md: 'flex' + } + }} + /> + {currentJourney != null ? ( + + ) : ( + + + {t('Start chatting to see your journey preview')} + + + )} + + + + ) +} + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + const journeyId = Array.isArray(ctx.query?.journeyId) + ? ctx.query.journeyId[0] + : ctx.query?.journeyId + + if (journeyId == null) return { notFound: true as const } + + const tokens = await getAuthTokens(ctx) + if (tokens == null) return redirectToLogin(ctx) + + const user = toUser(tokens) + const { flags, redirect, translations } = await initAndAuthApp({ + user, + locale: ctx.locale, + resolvedUrl: ctx.resolvedUrl + }) + + if (redirect != null) return { redirect } + + return { + props: { + userSerialized: JSON.stringify(user), + ...translations, + flags + } + } +} + +export default AiEditorPage diff --git a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx new file mode 100644 index 00000000000..42d94208785 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx @@ -0,0 +1,450 @@ +import { gql, useMutation } from '@apollo/client' +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' +import Typography from '@mui/material/Typography' +import { SxProps } from '@mui/material/styles' +import { useTranslation } from 'next-i18next' +import { ReactElement, useCallback, useEffect, useReducer, useRef } from 'react' + +import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' + +import { AiChatInput } from './AiChatInput' +import { AiChatMessage, ChatMessage } from './AiChatMessage' + +export const JOURNEY_AI_EDIT = gql` + mutation JourneyAiEdit($input: JourneyAiEditInput!) { + journeyAiEdit(input: $input) { + reply + proposedJourney + } + } +` + +export const JOURNEY_SIMPLE_UPDATE = gql` + mutation JourneySimpleUpdateFromAiEditor($id: ID!, $journey: Json!) { + journeySimpleUpdate(id: $id, journey: $journey) + } +` + +export interface AiState { + status: 'idle' | 'loading' | 'proposal' | 'error' + affectedCardIds: string[] +} + +interface AiChatProps { + journeyId: string + currentJourney: JourneySimple | null + selectedCardId?: string | null + selectedCardIndex?: number | null + onClearSelectedCard?: () => void + onAiState: (state: AiState) => void + onJourneyUpdated: (journey: JourneySimple) => void + sx?: SxProps +} + +interface AiChatState { + status: 'idle' | 'loading' | 'error' + messages: ChatMessage[] + generationId: number + inputValue: string + errorMessage: string | null + applyingMessageId: number | null +} + +type AiChatAction = + | { type: 'SEND'; message: string } + | { + type: 'RECEIVE' + reply: string + proposedJourney: JourneySimple | null + diffSummary: string[] + } + | { type: 'ERROR'; errorMessage: string } + | { type: 'INPUT_CHANGE'; value: string } + | { type: 'APPLY_START'; messageGenerationId: number } + | { type: 'APPLY_SUCCESS'; messageGenerationId: number } + | { type: 'APPLY_ERROR'; messageGenerationId: number } + | { type: 'DISMISS'; messageGenerationId: number } + +function reducer(state: AiChatState, action: AiChatAction): AiChatState { + switch (action.type) { + case 'SEND': { + const userMessage: ChatMessage = { + role: 'user', + content: action.message, + generationId: state.generationId + 1 + } + return { + ...state, + status: 'loading', + messages: [...state.messages, userMessage], + generationId: state.generationId + 1, + inputValue: '', + errorMessage: null + } + } + case 'RECEIVE': { + const assistantMessage: ChatMessage = { + role: 'assistant', + content: action.reply, + proposedJourney: action.proposedJourney ?? undefined, + diffSummary: action.diffSummary, + generationId: state.generationId + } + return { + ...state, + status: 'idle', + messages: [...state.messages, assistantMessage] + } + } + case 'ERROR': { + return { + ...state, + status: 'idle', + errorMessage: action.errorMessage + } + } + case 'INPUT_CHANGE': { + return { ...state, inputValue: action.value } + } + case 'APPLY_START': { + return { ...state, applyingMessageId: action.messageGenerationId } + } + case 'APPLY_SUCCESS': { + return { + ...state, + applyingMessageId: null, + generationId: state.generationId + 1, + messages: state.messages.map((m) => + m.generationId === action.messageGenerationId + ? { ...m, applied: true } + : m + ) + } + } + case 'APPLY_ERROR': { + return { ...state, applyingMessageId: null } + } + case 'DISMISS': { + return { + ...state, + messages: state.messages.map((m) => + m.generationId === action.messageGenerationId + ? { ...m, dismissed: true } + : m + ) + } + } + default: + return state + } +} + +function describeJourneyDiff( + current: JourneySimple | null, + proposed: JourneySimple +): { affectedCardIds: string[]; summary: string[] } { + if (current == null) { + return { + affectedCardIds: proposed.cards.map((c) => c.id), + summary: ['Journey updated'] + } + } + + const affectedCardIds: string[] = [] + const summary: string[] = [] + + const currentMap = new Map(current.cards.map((c) => [c.id, c])) + const proposedMap = new Map(proposed.cards.map((c) => [c.id, c])) + + for (const proposedCard of proposed.cards) { + const currentCard = currentMap.get(proposedCard.id) + if (currentCard == null) { + affectedCardIds.push(proposedCard.id) + const idx = proposed.cards.indexOf(proposedCard) + 1 + summary.push(`New screen added at position ${idx}`) + } else if (JSON.stringify(currentCard) !== JSON.stringify(proposedCard)) { + affectedCardIds.push(proposedCard.id) + const idx = proposed.cards.indexOf(proposedCard) + 1 + summary.push(`Screen ${idx} updated`) + } + } + + for (const currentCard of current.cards) { + if (!proposedMap.has(currentCard.id)) { + const idx = current.cards.indexOf(currentCard) + 1 + summary.push(`Screen ${idx} removed`) + } + } + + if (current.title !== proposed.title) { + summary.push(`Title updated to "${proposed.title}"`) + } + + return { affectedCardIds, summary } +} + +const SUGGESTED_PROMPTS = [ + 'Add an introduction screen at the beginning', + 'Make the call-to-action more compelling', + 'What could I improve about this journey?' +] + +export function AiChat({ + journeyId, + currentJourney, + selectedCardId, + selectedCardIndex, + onClearSelectedCard, + onAiState, + onJourneyUpdated, + sx +}: AiChatProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const messagesEndRef = useRef(null) + + const [state, dispatch] = useReducer(reducer, { + status: 'idle', + messages: [], + generationId: 0, + inputValue: '', + errorMessage: null, + applyingMessageId: null + }) + + const [journeyAiEdit] = useMutation(JOURNEY_AI_EDIT) + const [journeySimpleUpdate] = useMutation(JOURNEY_SIMPLE_UPDATE) + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [state.messages]) + + const handleSend = useCallback(async () => { + if (state.status !== 'idle' || state.inputValue.trim().length === 0) return + + const message = state.inputValue.trim() + dispatch({ type: 'SEND', message }) + + const history = state.messages.slice(-10).map((m) => ({ + role: m.role, + content: m.content + })) + + onAiState({ + status: 'loading', + affectedCardIds: selectedCardId != null ? [selectedCardId] : [] + }) + + try { + const result = await journeyAiEdit({ + variables: { + input: { + journeyId, + message, + history, + selectedCardId: selectedCardId ?? null + } + } + }) + + const data = result.data?.journeyAiEdit + if (data == null) throw new Error('No response from AI') + + const proposedJourney = data.proposedJourney as JourneySimple | null + const diff = + proposedJourney != null + ? describeJourneyDiff(currentJourney, proposedJourney) + : { affectedCardIds: [], summary: [] } + + dispatch({ + type: 'RECEIVE', + reply: data.reply, + proposedJourney, + diffSummary: diff.summary + }) + + onAiState({ + status: proposedJourney != null ? 'proposal' : 'idle', + affectedCardIds: diff.affectedCardIds + }) + } catch { + dispatch({ + type: 'ERROR', + errorMessage: 'Failed to get a response. Please try again.' + }) + onAiState({ status: 'idle', affectedCardIds: [] }) + } + }, [ + state.status, + state.inputValue, + state.messages, + journeyId, + selectedCardId, + currentJourney, + journeyAiEdit, + onAiState + ]) + + const handleApply = useCallback( + async (message: ChatMessage) => { + if (message.proposedJourney == null) return + if (message.generationId !== state.generationId) return + + dispatch({ type: 'APPLY_START', messageGenerationId: message.generationId }) + + try { + await journeySimpleUpdate({ + variables: { + id: journeyId, + journey: message.proposedJourney + } + }) + dispatch({ + type: 'APPLY_SUCCESS', + messageGenerationId: message.generationId + }) + onJourneyUpdated(message.proposedJourney) + onAiState({ status: 'idle', affectedCardIds: [] }) + } catch { + dispatch({ + type: 'APPLY_ERROR', + messageGenerationId: message.generationId + }) + } + }, + [ + state.generationId, + journeyId, + journeySimpleUpdate, + onJourneyUpdated, + onAiState + ] + ) + + const handleDismiss = useCallback( + (message: ChatMessage) => { + dispatch({ type: 'DISMISS', messageGenerationId: message.generationId }) + onAiState({ status: 'idle', affectedCardIds: [] }) + }, + [onAiState] + ) + + const isEmpty = state.messages.length === 0 + + return ( + + {/* Header */} + + + {t('AI Editor')} + + + {/* Messages area */} + + {isEmpty ? ( + + + + {t('Describe changes to your journey in plain language')} + + + {SUGGESTED_PROMPTS.map((prompt) => ( + + dispatch({ type: 'INPUT_CHANGE', value: prompt }) + } + sx={{ + cursor: 'pointer', + p: 1, + borderRadius: 1, + border: 1, + borderColor: 'divider', + color: 'text.secondary', + '&:hover': { bgcolor: 'action.hover' } + }} + > + {prompt} + + ))} + + + ) : ( + state.messages.map((message, i) => ( + + )) + )} + {state.status === 'loading' && ( + + + + {t('Thinking...')} + + + )} + {state.errorMessage != null && ( + + {state.errorMessage} + + )} +
+ + + {/* Input */} + dispatch({ type: 'INPUT_CHANGE', value })} + onSend={handleSend} + disabled={state.status === 'loading'} + selectedCardId={selectedCardId} + selectedCardIndex={selectedCardIndex} + onClearSelectedCard={onClearSelectedCard} + /> + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx new file mode 100644 index 00000000000..57f7f52f9a4 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx @@ -0,0 +1,95 @@ +import SendRoundedIcon from '@mui/icons-material/SendRounded' +import Box from '@mui/material/Box' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import { useTranslation } from 'next-i18next' +import { KeyboardEvent, ReactElement } from 'react' + +interface AiChatInputProps { + value: string + onChange: (value: string) => void + onSend: () => void + disabled: boolean + selectedCardId?: string | null + selectedCardIndex?: number | null + onClearSelectedCard?: () => void +} + +const MAX_LENGTH = 2000 + +export function AiChatInput({ + value, + onChange, + onSend, + disabled, + selectedCardId, + selectedCardIndex, + onClearSelectedCard +}: AiChatInputProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const charsLeft = MAX_LENGTH - value.length + const nearLimit = charsLeft < 200 + + function handleKeyDown(e: KeyboardEvent): void { + if ( + (e.metaKey || e.ctrlKey) && + e.key === 'Enter' && + !disabled && + value.trim().length > 0 + ) { + e.preventDefault() + onSend() + } + } + + return ( + + {selectedCardId != null && selectedCardIndex != null && ( + + + + )} + + onChange(e.target.value)} + onKeyDown={handleKeyDown} + disabled={disabled} + inputProps={{ maxLength: MAX_LENGTH }} + size="small" + /> + + + + + {nearLimit && ( + + {t('{{n}} characters remaining', { n: charsLeft })} + + )} + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatMessage.tsx b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatMessage.tsx new file mode 100644 index 00000000000..ad5918d77f1 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatMessage.tsx @@ -0,0 +1,175 @@ +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import CircularProgress from '@mui/material/CircularProgress' +import Divider from '@mui/material/Divider' +import Paper from '@mui/material/Paper' +import Typography from '@mui/material/Typography' +import { useTranslation } from 'next-i18next' +import { ReactElement } from 'react' + +import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' + +export interface ChatMessage { + role: 'user' | 'assistant' + content: string + proposedJourney?: JourneySimple + generationId: number + applied?: boolean + dismissed?: boolean + diffSummary?: string[] +} + +interface AiChatMessageProps { + message: ChatMessage + currentGenerationId: number + onApply: (message: ChatMessage) => void + onDismiss: (message: ChatMessage) => void + applying?: boolean +} + +export function AiChatMessage({ + message, + currentGenerationId, + onApply, + onDismiss, + applying +}: AiChatMessageProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const isUser = message.role === 'user' + const isStale = message.generationId !== currentGenerationId + const hasProposal = + message.proposedJourney != null && + !message.applied && + !message.dismissed + + return ( + + + + {!isUser && ( + + + + {t('AI Assistant')} + + + )} + + {message.content} + + + + {hasProposal && ( + + {isStale ? ( + + {t('This proposal is outdated')} + + ) : ( + <> + + + {t('Proposed changes')} + + {message.diffSummary != null && + message.diffSummary.length > 0 && ( + + {message.diffSummary.map((item, i) => ( + + • {item} + + ))} + + )} + + + + + + + )} + + )} + + {message.applied === true && ( + + {t('✓ Applied')} + + )} + {message.dismissed === true && ( + + {t('Dismissed')} + + )} + + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx new file mode 100644 index 00000000000..a5ccb650a39 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx @@ -0,0 +1,230 @@ +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import { SxProps } from '@mui/material/styles' +import { useTranslation } from 'next-i18next' +import { ReactElement } from 'react' + +import { + JourneySimple, + JourneySimpleCard +} from '@core/shared/ai/journeySimpleTypes' + +import { AiState } from './AiChat/AiChat' + +interface AiEditorCardPreviewProps { + journey: JourneySimple + selectedCardId: string | null + aiState: AiState + sx?: SxProps +} + +function CardContent({ + card, + cardIndex +}: { + card: JourneySimpleCard + cardIndex: number +}): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + + return ( + + {/* Background image overlay */} + {card.backgroundImage != null && ( + + )} + + + + {t('Screen {{n}}', { n: cardIndex + 1 })} + + + {card.video != null && ( + + + 🎬 {t('Video')} + + + )} + + {card.image != null && ( + + )} + + {card.heading != null && ( + + {card.heading} + + )} + + {card.text != null && ( + + {card.text} + + )} + + + + {card.button != null && ( + + {card.button.text} + + )} + + {card.poll != null && card.poll.length > 0 && ( + + {card.poll.map((option, i) => ( + + {option.text} + + ))} + + )} + + + ) +} + +export function AiEditorCardPreview({ + journey, + selectedCardId, + aiState, + sx +}: AiEditorCardPreviewProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + const cardIndex = journey.cards.findIndex((c) => c.id === selectedCardId) + const card = cardIndex >= 0 ? journey.cards[cardIndex] : null + const isAffected = + selectedCardId != null && + aiState.affectedCardIds.includes(selectedCardId) + + return ( + + {card == null ? ( + + {t('Click a screen in the map below to preview it')} + + ) : ( + + + {isAffected && aiState.status === 'proposal' && ( + + AI CHANGE + + )} + + )} + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx new file mode 100644 index 00000000000..0fb6ff0cdb9 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx @@ -0,0 +1,235 @@ +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' +import Box from '@mui/material/Box' +import Paper from '@mui/material/Paper' +import Typography from '@mui/material/Typography' +import { SxProps } from '@mui/material/styles' +import { ReactElement } from 'react' + +import { JourneySimple, JourneySimpleCard } from '@core/shared/ai/journeySimpleTypes' + +import { AiState } from './AiChat/AiChat' + +interface AiEditorFlowMapProps { + journey: JourneySimple + selectedCardId: string | null + aiState: AiState + onCardSelect: (cardId: string) => void + sx?: SxProps +} + +interface CardNode { + card: JourneySimpleCard + index: number +} + +function getCardConnections( + cards: JourneySimpleCard[] +): Array<{ from: string; to: string }> { + const connections: Array<{ from: string; to: string }> = [] + + for (const card of cards) { + if (card.defaultNextCard != null) { + connections.push({ from: card.id, to: card.defaultNextCard }) + } + if (card.button?.nextCard != null) { + connections.push({ from: card.id, to: card.button.nextCard }) + } + if (card.poll != null) { + for (const option of card.poll) { + if (option.nextCard != null) { + connections.push({ from: card.id, to: option.nextCard }) + } + } + } + } + + return connections +} + +export function AiEditorFlowMap({ + journey, + selectedCardId, + aiState, + onCardSelect, + sx +}: AiEditorFlowMapProps): ReactElement { + const CARD_WIDTH = 120 + const CARD_HEIGHT = 72 + const H_GAP = 48 + const V_GAP = 32 + const CARDS_PER_ROW = 3 + + const nodes: CardNode[] = journey.cards.map((card, index) => ({ + card, + index + })) + + const useStoredCoords = journey.cards.every( + (c) => typeof c.x === 'number' && typeof c.y === 'number' + ) + + function getPos(node: CardNode): { x: number; y: number } { + if (useStoredCoords) { + return { + x: node.card.x * (CARD_WIDTH + H_GAP), + y: node.card.y * (CARD_HEIGHT + V_GAP) + } + } + const col = node.index % CARDS_PER_ROW + const row = Math.floor(node.index / CARDS_PER_ROW) + return { + x: col * (CARD_WIDTH + H_GAP) + H_GAP, + y: row * (CARD_HEIGHT + V_GAP) + V_GAP + } + } + + const positions = new Map(nodes.map((n) => [n.card.id, getPos(n)])) + + const maxX = + Math.max(...Array.from(positions.values()).map((p) => p.x)) + + CARD_WIDTH + + H_GAP + const maxY = + Math.max(...Array.from(positions.values()).map((p) => p.y)) + + CARD_HEIGHT + + V_GAP + + const connections = getCardConnections(journey.cards) + + return ( + + + {/* SVG arrows */} + + {connections.map(({ from, to }, i) => { + const fromPos = positions.get(from) + const toPos = positions.get(to) + if (fromPos == null || toPos == null) return null + + const x1 = fromPos.x + CARD_WIDTH / 2 + const y1 = fromPos.y + CARD_HEIGHT + const x2 = toPos.x + CARD_WIDTH / 2 + const y2 = toPos.y + + return ( + + + + + + + + + ) + })} + + + {/* Card nodes */} + {nodes.map((node) => { + const pos = getPos(node) + const isSelected = selectedCardId === node.card.id + const isAffected = aiState.affectedCardIds.includes(node.card.id) + const isLoading = aiState.status === 'loading' && isAffected + const isProposal = aiState.status === 'proposal' && isAffected + + return ( + onCardSelect(node.card.id)} + data-testid={`FlowMapCard-${node.card.id}`} + sx={{ + position: 'absolute', + left: pos.x, + top: pos.y, + width: CARD_WIDTH, + height: CARD_HEIGHT, + p: 1, + cursor: 'pointer', + overflow: 'hidden', + border: 2, + borderColor: isSelected + ? 'primary.main' + : isProposal + ? 'primary.light' + : isLoading + ? 'warning.light' + : 'transparent', + animation: isLoading + ? 'pulse 1.5s ease-in-out infinite' + : 'none', + '@keyframes pulse': { + '0%, 100%': { opacity: 1 }, + '50%': { opacity: 0.6 } + }, + '&:hover': { borderColor: 'primary.light', elevation: 2 } + }} + > + + + {node.index + 1} + + + {node.card.heading ?? node.card.text ?? '(empty)'} + + {isProposal && ( + + )} + + + ) + })} + + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorHeader.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorHeader.tsx new file mode 100644 index 00000000000..59afee4c699 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorHeader.tsx @@ -0,0 +1,87 @@ +import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded' +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import IconButton from '@mui/material/IconButton' +import Tooltip from '@mui/material/Tooltip' +import Typography from '@mui/material/Typography' +import NextLink from 'next/link' +import { useTranslation } from 'next-i18next' +import { ReactElement } from 'react' + +interface AiEditorHeaderProps { + journeyId: string + journeyTitle: string +} + +export function AiEditorHeader({ + journeyId, + journeyTitle +}: AiEditorHeaderProps): ReactElement { + const { t } = useTranslation('apps-journeys-admin') + + return ( + + + + + + + + + + + {journeyTitle} + + + — {t('AI Editor')} + + + + + + + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx new file mode 100644 index 00000000000..b9b48921de5 --- /dev/null +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx @@ -0,0 +1,57 @@ +import Box from '@mui/material/Box' +import { SxProps } from '@mui/material/styles' +import { ReactElement, useState } from 'react' + +import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' + +import { AiState } from './AiChat/AiChat' +import { AiEditorCardPreview } from './AiEditorCardPreview' +import { AiEditorFlowMap } from './AiEditorFlowMap' + +interface AiEditorPreviewProps { + journey: JourneySimple + aiState: AiState + onSelectedCardChange: (cardId: string | null) => void + sx?: SxProps +} + +export function AiEditorPreview({ + journey, + aiState, + onSelectedCardChange, + sx +}: AiEditorPreviewProps): ReactElement { + const [selectedCardId, setSelectedCardId] = useState(null) + + function handleCardSelect(cardId: string): void { + const next = selectedCardId === cardId ? null : cardId + setSelectedCardId(next) + onSelectedCardChange(next) + } + + return ( + + + + + ) +} diff --git a/apps/journeys-admin/src/components/Editor/Toolbar/Toolbar.tsx b/apps/journeys-admin/src/components/Editor/Toolbar/Toolbar.tsx index e4e62d525fe..d783d2e0422 100644 --- a/apps/journeys-admin/src/components/Editor/Toolbar/Toolbar.tsx +++ b/apps/journeys-admin/src/components/Editor/Toolbar/Toolbar.tsx @@ -1,4 +1,5 @@ import { gql, useApolloClient, useMutation } from '@apollo/client' +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted' import Box from '@mui/material/Box' import Button from '@mui/material/Button' @@ -345,6 +346,17 @@ export function Toolbar({ user }: ToolbarProps): ReactElement { alignItems="center" > + {journey != null && journey.template !== true && smUp && ( + + + + + + )} diff --git a/apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/DefaultMenu.tsx b/apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/DefaultMenu.tsx index 5140d7786ad..e5db5d49a43 100644 --- a/apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/DefaultMenu.tsx +++ b/apps/journeys-admin/src/components/JourneyList/JourneyCard/JourneyCardMenu/DefaultMenu/DefaultMenu.tsx @@ -1,4 +1,5 @@ import { ApolloQueryResult, gql, useQuery } from '@apollo/client' +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' import Divider from '@mui/material/Divider' import NextLink from 'next/link' import { useTranslation } from 'next-i18next' @@ -211,6 +212,14 @@ export function DefaultMenu({ setHasOpenDialog?.(true) }} /> + {template !== true && ( + + } + /> + + )} {template !== true && ( Date: Wed, 18 Mar 2026 00:09:32 +0000 Subject: [PATCH 02/10] feat(journeys): generate schemas and types for journeyAiEdit Run api-journeys-modern:generate-graphql to export JourneyAiEditInput, JourneyAiEditResult, and journeyAiEdit mutation to the subgraph schema. Run api-gateway:generate-graphql to compose updated supergraph schema. Run journeys-admin:codegen to generate TypeScript types for the new mutation and query. Co-Authored-By: Claude Sonnet 4.6 (200K context) --- apis/api-gateway/schema.graphql | 13 ++++++++++ apis/api-journeys-modern/schema.graphql | 13 ++++++++++ .../__generated__/GetAiEditorJourney.ts | 26 +++++++++++++++++++ .../GoogleSheetsSyncsForDoneScreen.ts | 24 +++++++++++++++++ .../__generated__/JourneyAiEdit.ts | 24 +++++++++++++++++ .../JourneySimpleUpdateFromAiEditor.ts | 17 ++++++++++++ .../__generated__/globalTypes.ts | 7 +++++ .../pages/journeys/[journeyId]/ai.tsx | 9 ++++++- .../src/components/AiEditor/AiChat/AiChat.tsx | 18 +++++++++++-- 9 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 apps/journeys-admin/__generated__/GetAiEditorJourney.ts create mode 100644 apps/journeys-admin/__generated__/GoogleSheetsSyncsForDoneScreen.ts create mode 100644 apps/journeys-admin/__generated__/JourneyAiEdit.ts create mode 100644 apps/journeys-admin/__generated__/JourneySimpleUpdateFromAiEditor.ts diff --git a/apis/api-gateway/schema.graphql b/apis/api-gateway/schema.graphql index 6e44e126f5d..e9c42b2e83e 100644 --- a/apis/api-gateway/schema.graphql +++ b/apis/api-gateway/schema.graphql @@ -410,6 +410,7 @@ type Mutation @join__type(graph: API_ANALYTICS) @join__type(graph: API_JOURNEYS integrationGoogleCreate(input: IntegrationGoogleCreateInput!) : IntegrationGoogle! @join__field(graph: API_JOURNEYS_MODERN) integrationGoogleUpdate(id: ID!, input: IntegrationGoogleUpdateInput!) : IntegrationGoogle! @join__field(graph: API_JOURNEYS_MODERN) integrationDelete(id: ID!) : Integration! @join__field(graph: API_JOURNEYS_MODERN) + journeyAiEdit(input: JourneyAiEditInput!) : JourneyAiEditResult! @join__field(graph: API_JOURNEYS_MODERN) journeyAiTranslateCreate(input: JourneyAiTranslateInput!) : Journey! @join__field(graph: API_JOURNEYS_MODERN) createJourneyEventsExportLog(input: JourneyEventsExportLogInput!) : JourneyEventsExportLog! @join__field(graph: API_JOURNEYS_MODERN) journeyLanguageAiDetect(input: MutationJourneyLanguageAiDetectInput!) : Boolean! @join__field(graph: API_JOURNEYS_MODERN) @@ -2388,6 +2389,11 @@ type GoogleSheetsSync @join__type(graph: API_JOURNEYS_MODERN) { journey: Journey! } +type JourneyAiEditResult @join__type(graph: API_JOURNEYS_MODERN) { + reply: String + proposedJourney: Json +} + type JourneyAiTranslateProgress @join__type(graph: API_JOURNEYS_MODERN) { """ Translation progress as a percentage (0-100) @@ -4803,6 +4809,13 @@ input IntegrationGoogleUpdateInput @join__type(graph: API_JOURNEYS_MODERN) { redirectUri: String! } +input JourneyAiEditInput @join__type(graph: API_JOURNEYS_MODERN) { + journeyId: ID! + message: String! + history: Json + selectedCardId: String +} + input JourneyAiTranslateInput @join__type(graph: API_JOURNEYS_MODERN) { journeyId: ID! name: String! diff --git a/apis/api-journeys-modern/schema.graphql b/apis/api-journeys-modern/schema.graphql index a45f630917e..6f2406b6e8f 100644 --- a/apis/api-journeys-modern/schema.graphql +++ b/apis/api-journeys-modern/schema.graphql @@ -823,6 +823,18 @@ type Journey journeyCollections: [JourneyCollection!]! } +input JourneyAiEditInput { + journeyId: ID! + message: String! + history: Json + selectedCardId: String +} + +type JourneyAiEditResult { + reply: String + proposedJourney: Json +} + input JourneyAiTranslateInput { journeyId: ID! name: String! @@ -1393,6 +1405,7 @@ type Mutation { integrationGoogleCreate(input: IntegrationGoogleCreateInput!): IntegrationGoogle! integrationGoogleUpdate(id: ID!, input: IntegrationGoogleUpdateInput!): IntegrationGoogle! integrationDelete(id: ID!): Integration! + journeyAiEdit(input: JourneyAiEditInput!): JourneyAiEditResult! journeyAiTranslateCreate(input: JourneyAiTranslateInput!): Journey! createJourneyEventsExportLog(input: JourneyEventsExportLogInput!): JourneyEventsExportLog! journeyLanguageAiDetect(input: MutationJourneyLanguageAiDetectInput!): Boolean! diff --git a/apps/journeys-admin/__generated__/GetAiEditorJourney.ts b/apps/journeys-admin/__generated__/GetAiEditorJourney.ts new file mode 100644 index 00000000000..e53516ca024 --- /dev/null +++ b/apps/journeys-admin/__generated__/GetAiEditorJourney.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetAiEditorJourney +// ==================================================== + +export interface GetAiEditorJourney_journey { + __typename: "Journey"; + id: string; + /** + * private title for creators + */ + title: string; + description: string | null; +} + +export interface GetAiEditorJourney { + journey: GetAiEditorJourney_journey; +} + +export interface GetAiEditorJourneyVariables { + id: string; +} diff --git a/apps/journeys-admin/__generated__/GoogleSheetsSyncsForDoneScreen.ts b/apps/journeys-admin/__generated__/GoogleSheetsSyncsForDoneScreen.ts new file mode 100644 index 00000000000..d5a7147e851 --- /dev/null +++ b/apps/journeys-admin/__generated__/GoogleSheetsSyncsForDoneScreen.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { GoogleSheetsSyncsFilter } from "./globalTypes"; + +// ==================================================== +// GraphQL query operation: GoogleSheetsSyncsForDoneScreen +// ==================================================== + +export interface GoogleSheetsSyncsForDoneScreen_googleSheetsSyncs { + __typename: "GoogleSheetsSync"; + id: string; + deletedAt: any | null; +} + +export interface GoogleSheetsSyncsForDoneScreen { + googleSheetsSyncs: GoogleSheetsSyncsForDoneScreen_googleSheetsSyncs[]; +} + +export interface GoogleSheetsSyncsForDoneScreenVariables { + filter: GoogleSheetsSyncsFilter; +} diff --git a/apps/journeys-admin/__generated__/JourneyAiEdit.ts b/apps/journeys-admin/__generated__/JourneyAiEdit.ts new file mode 100644 index 00000000000..95fc7a91a2f --- /dev/null +++ b/apps/journeys-admin/__generated__/JourneyAiEdit.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { JourneyAiEditInput } from "./globalTypes"; + +// ==================================================== +// GraphQL mutation operation: JourneyAiEdit +// ==================================================== + +export interface JourneyAiEdit_journeyAiEdit { + __typename: "JourneyAiEditResult"; + reply: string | null; + proposedJourney: any | null; +} + +export interface JourneyAiEdit { + journeyAiEdit: JourneyAiEdit_journeyAiEdit; +} + +export interface JourneyAiEditVariables { + input: JourneyAiEditInput; +} diff --git a/apps/journeys-admin/__generated__/JourneySimpleUpdateFromAiEditor.ts b/apps/journeys-admin/__generated__/JourneySimpleUpdateFromAiEditor.ts new file mode 100644 index 00000000000..b50da9abd5f --- /dev/null +++ b/apps/journeys-admin/__generated__/JourneySimpleUpdateFromAiEditor.ts @@ -0,0 +1,17 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: JourneySimpleUpdateFromAiEditor +// ==================================================== + +export interface JourneySimpleUpdateFromAiEditor { + journeySimpleUpdate: any | null; +} + +export interface JourneySimpleUpdateFromAiEditorVariables { + id: string; + journey: any; +} diff --git a/apps/journeys-admin/__generated__/globalTypes.ts b/apps/journeys-admin/__generated__/globalTypes.ts index 47d4d4d4700..1b0f76c9ec6 100644 --- a/apps/journeys-admin/__generated__/globalTypes.ts +++ b/apps/journeys-admin/__generated__/globalTypes.ts @@ -580,6 +580,13 @@ export interface IntegrationGrowthSpacesUpdateInput { accessSecret: string; } +export interface JourneyAiEditInput { + journeyId: string; + message: string; + history?: any | null; + selectedCardId?: string | null; +} + export interface JourneyCollectionCreateInput { id?: string | null; teamId: string; diff --git a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx index 587c9e73465..74638882b76 100644 --- a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx +++ b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx @@ -10,6 +10,10 @@ import { ReactElement, useCallback, useState } from 'react' import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' +import { + GetAiEditorJourney, + GetAiEditorJourneyVariables +} from '../../../__generated__/GetAiEditorJourney' import { AiChat, AiState } from '../../../src/components/AiEditor/AiChat/AiChat' import { AiEditorHeader } from '../../../src/components/AiEditor/AiEditorHeader' import { AiEditorPreview } from '../../../src/components/AiEditor/AiEditorPreview' @@ -44,7 +48,10 @@ function AiEditorPage(): ReactElement { }) const [selectedCardId, setSelectedCardId] = useState(null) - const { data, loading } = useQuery(GET_AI_EDITOR_JOURNEY, { + const { data, loading } = useQuery< + GetAiEditorJourney, + GetAiEditorJourneyVariables + >(GET_AI_EDITOR_JOURNEY, { variables: { id: journeyId }, skip: journeyId == null }) diff --git a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx index 42d94208785..f1186d15bf3 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx @@ -4,6 +4,15 @@ import Box from '@mui/material/Box' import CircularProgress from '@mui/material/CircularProgress' import Typography from '@mui/material/Typography' import { SxProps } from '@mui/material/styles' + +import { + JourneyAiEdit, + JourneyAiEditVariables +} from '../../../../__generated__/JourneyAiEdit' +import { + JourneySimpleUpdateFromAiEditor, + JourneySimpleUpdateFromAiEditorVariables +} from '../../../../__generated__/JourneySimpleUpdateFromAiEditor' import { useTranslation } from 'next-i18next' import { ReactElement, useCallback, useEffect, useReducer, useRef } from 'react' @@ -213,8 +222,13 @@ export function AiChat({ applyingMessageId: null }) - const [journeyAiEdit] = useMutation(JOURNEY_AI_EDIT) - const [journeySimpleUpdate] = useMutation(JOURNEY_SIMPLE_UPDATE) + const [journeyAiEdit] = useMutation( + JOURNEY_AI_EDIT + ) + const [journeySimpleUpdate] = useMutation< + JourneySimpleUpdateFromAiEditor, + JourneySimpleUpdateFromAiEditorVariables + >(JOURNEY_SIMPLE_UPDATE) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) From 01250fc9e4077b0228d290c505acea4c44edbec1 Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 18 Mar 2026 00:19:57 +0000 Subject: [PATCH 03/10] fix(journeyAiEdit): convert plain errors to GraphQLErrors to prevent Yoga masking --- .../src/schema/journey/simple/getSimpleJourney.ts | 5 ++++- .../src/schema/journeyAiEdit/journeyAiEdit.ts | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apis/api-journeys-modern/src/schema/journey/simple/getSimpleJourney.ts b/apis/api-journeys-modern/src/schema/journey/simple/getSimpleJourney.ts index b5482661bb2..36102f7df8f 100644 --- a/apis/api-journeys-modern/src/schema/journey/simple/getSimpleJourney.ts +++ b/apis/api-journeys-modern/src/schema/journey/simple/getSimpleJourney.ts @@ -20,7 +20,10 @@ export async function getSimpleJourney( } } }) - if (!journey) throw new Error('Journey not found') + if (!journey) + throw new GraphQLError('Journey not found', { + extensions: { code: 'NOT_FOUND' } + }) const simpleJourney = simplifyJourney(journey) const result = journeySimpleSchema.safeParse(simpleJourney) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts index 114f066c217..3ef30e2271c 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -109,7 +109,16 @@ builder.mutationField('journeyAiEdit', (t) => } // 3. Fetch simple journey representation - const currentJourney = await getSimpleJourney(input.journeyId) + let currentJourney: JourneySimple + try { + currentJourney = await getSimpleJourney(input.journeyId) + } catch (error) { + if (error instanceof GraphQLError) throw error + console.error('journeyAiEdit: failed to load journey', error) + throw new GraphQLError('Failed to load journey data. Please try again.', { + extensions: { code: 'INTERNAL_SERVER_ERROR' } + }) + } // 4. Prune history to last 10 turns const rawHistory = From d8ae5182a30d59d7589921353a865f8d9d866c3b Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 18 Mar 2026 00:25:10 +0000 Subject: [PATCH 04/10] fix(journeyAiEdit): replace Json history field with typed MessageHistoryItem list --- apis/api-gateway/schema.graphql | 7 ++++++- apis/api-journeys-modern/schema.graphql | 7 ++++++- .../src/schema/journeyAiEdit/journeyAiEdit.ts | 15 +++++++++++---- apps/journeys-admin/__generated__/globalTypes.ts | 7 ++++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apis/api-gateway/schema.graphql b/apis/api-gateway/schema.graphql index e9c42b2e83e..21cd03ec546 100644 --- a/apis/api-gateway/schema.graphql +++ b/apis/api-gateway/schema.graphql @@ -4812,7 +4812,7 @@ input IntegrationGoogleUpdateInput @join__type(graph: API_JOURNEYS_MODERN) { input JourneyAiEditInput @join__type(graph: API_JOURNEYS_MODERN) { journeyId: ID! message: String! - history: Json + history: [MessageHistoryItem!] selectedCardId: String } @@ -4873,6 +4873,11 @@ input LinkActionInput @join__type(graph: API_JOURNEYS_MODERN) { parentStepId: String } +input MessageHistoryItem @join__type(graph: API_JOURNEYS_MODERN) { + role: String! + content: String! +} + input MultiselectBlockCreateInput @join__type(graph: API_JOURNEYS_MODERN) { id: ID journeyId: ID! diff --git a/apis/api-journeys-modern/schema.graphql b/apis/api-journeys-modern/schema.graphql index 6f2406b6e8f..74f54fd2dca 100644 --- a/apis/api-journeys-modern/schema.graphql +++ b/apis/api-journeys-modern/schema.graphql @@ -826,7 +826,7 @@ type Journey input JourneyAiEditInput { journeyId: ID! message: String! - history: Json + history: [MessageHistoryItem!] selectedCardId: String } @@ -1245,6 +1245,11 @@ input LinkActionInput { union MediaVideo = MuxVideo | Video | YouTube +input MessageHistoryItem { + role: String! + content: String! +} + enum MessagePlatform { facebook telegram diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts index 3ef30e2271c..e9772ac5ab8 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -38,12 +38,19 @@ builder.objectType(JourneyAiEditResultRef, { }) }) +const MessageHistoryItem = builder.inputType('MessageHistoryItem', { + fields: (t) => ({ + role: t.string({ required: true }), + content: t.string({ required: true }) + }) +}) + // Input type const JourneyAiEditInput = builder.inputType('JourneyAiEditInput', { fields: (t) => ({ journeyId: t.id({ required: true }), message: t.string({ required: true }), - history: t.field({ type: 'Json', required: false }), + history: t.field({ type: [MessageHistoryItem], required: false }), selectedCardId: t.string({ required: false }) }) }) @@ -121,9 +128,9 @@ builder.mutationField('journeyAiEdit', (t) => } // 4. Prune history to last 10 turns - const rawHistory = - (input.history as ModelMessage[] | null | undefined) ?? [] - const prunedHistory = rawHistory.slice(-10) + const prunedHistory = (input.history ?? []) + .slice(-10) + .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content })) // 5. Harden user message const hardenedMessage = hardenPrompt(input.message) diff --git a/apps/journeys-admin/__generated__/globalTypes.ts b/apps/journeys-admin/__generated__/globalTypes.ts index 1b0f76c9ec6..9fc6dfbd5c8 100644 --- a/apps/journeys-admin/__generated__/globalTypes.ts +++ b/apps/journeys-admin/__generated__/globalTypes.ts @@ -583,7 +583,7 @@ export interface IntegrationGrowthSpacesUpdateInput { export interface JourneyAiEditInput { journeyId: string; message: string; - history?: any | null; + history?: MessageHistoryItem[] | null; selectedCardId?: string | null; } @@ -745,6 +745,11 @@ export interface MeInput { app?: App | null; } +export interface MessageHistoryItem { + role: string; + content: string; +} + export interface MultiselectBlockCreateInput { id?: string | null; journeyId: string; From e7073644fdfaf3830d59b1306df0245093b9fcac Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 18 Mar 2026 00:40:27 +0000 Subject: [PATCH 05/10] fix(journeyAiEdit): replace discriminated union with flat schema and use gemini-2.0-flash --- .../src/schema/journeyAiEdit/journeyAiEdit.ts | 40 +++++++------------ .../src/schema/journeyAiEdit/prompts.ts | 4 +- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts index e9772ac5ab8..114a1556d06 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -55,22 +55,19 @@ const JourneyAiEditInput = builder.inputType('JourneyAiEditInput', { }) }) -// Discriminated union schema for AI response -const journeyAiEditSchema = z.discriminatedUnion('hasChanges', [ - z.object({ - hasChanges: z.literal(true), - reply: z - .string() - .describe('Plain language explanation of what was changed and why'), - journey: journeySimpleSchemaUpdate.describe( - 'Full updated journey with all changes applied' +// Flat schema for AI response — avoids boolean enum which Gemini rejects +const journeyAiEditSchema = z.object({ + reply: z + .string() + .describe( + 'Plain language explanation of what was changed and why, or suggestions/answer if no changes' + ), + journey: journeySimpleSchemaUpdate + .nullable() + .describe( + 'Full updated journey with all changes applied, or null if no structural changes are needed' ) - }), - z.object({ - hasChanges: z.literal(false), - reply: z.string().describe('Plain language suggestions or answer') - }) -]) +}) builder.mutationField('journeyAiEdit', (t) => t.withAuth({ isAuthenticated: true }).field({ @@ -145,7 +142,7 @@ builder.mutationField('journeyAiEdit', (t) => let aiResult: z.infer try { const { object } = await generateObject({ - model: google('gemini-2.5-flash-preview-04-17'), + model: google('gemini-2.0-flash'), system: systemPrompt, messages: [ ...prunedHistory, @@ -180,20 +177,13 @@ builder.mutationField('journeyAiEdit', (t) => userId: context.user.id, journeyId: input.journeyId, timestamp: new Date().toISOString(), - hadProposal: aiResult.hasChanges + hadProposal: aiResult.journey != null }) // 9. Return result - if (aiResult.hasChanges) { - return { - reply: aiResult.reply, - proposedJourney: aiResult.journey as JourneySimple - } - } - return { reply: aiResult.reply, - proposedJourney: null + proposedJourney: (aiResult.journey as JourneySimple) ?? null } } }) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts index 03c2babb7f7..f7de5222728 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/prompts.ts @@ -24,8 +24,8 @@ IMPORTANT LANGUAGE RULES: - Reply in the same language the user writes in OUTPUT CONTRACT: -- When making changes, set hasChanges: true and return the COMPLETE updated journey JSON in the journey field -- When only giving advice or answering questions, set hasChanges: false (no journey field needed) +- When making changes, return the COMPLETE updated journey JSON in the journey field +- When only giving advice or answering questions, set journey to null - Always include a clear, plain-language explanation in the reply field CURRENT JOURNEY STATE: From 7894847ba83932fd375798b8126229e9861090f0 Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 18 Mar 2026 00:58:54 +0000 Subject: [PATCH 06/10] fix(journeyAiEdit): use base schema for generation and sanitize empty video fields --- .../src/schema/journeyAiEdit/journeyAiEdit.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts index 114a1556d06..5a468d01e5c 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -7,7 +7,8 @@ import { prisma } from '@core/prisma/journeys/client' import { hardenPrompt } from '@core/shared/ai/prompts' import { JourneySimple, - journeySimpleSchemaUpdate + JourneySimpleCard, + journeySimpleSchema } from '@core/shared/ai/journeySimpleTypes' import { Action, ability, subject } from '../../lib/auth/ability' @@ -55,20 +56,36 @@ const JourneyAiEditInput = builder.inputType('JourneyAiEditInput', { }) }) -// Flat schema for AI response — avoids boolean enum which Gemini rejects +// Use the base schema (no superRefine) so Zod validation doesn't reject +// model output that has minor issues (e.g. empty video urls on non-video cards). +// We sanitize the output ourselves after generation. const journeyAiEditSchema = z.object({ reply: z .string() .describe( 'Plain language explanation of what was changed and why, or suggestions/answer if no changes' ), - journey: journeySimpleSchemaUpdate + journey: journeySimpleSchema .nullable() .describe( 'Full updated journey with all changes applied, or null if no structural changes are needed' ) }) +// Strip video fields with empty URLs that the model sometimes emits on non-video cards +function sanitizeJourney(journey: JourneySimple): JourneySimple { + return { + ...journey, + cards: journey.cards.map((card): JourneySimpleCard => { + if (card.video != null && !card.video.url) { + const { video: _video, ...rest } = card + return rest + } + return card + }) + } +} + builder.mutationField('journeyAiEdit', (t) => t.withAuth({ isAuthenticated: true }).field({ type: JourneyAiEditResultRef, @@ -181,9 +198,10 @@ builder.mutationField('journeyAiEdit', (t) => }) // 9. Return result + const rawJourney = aiResult.journey as JourneySimple | null return { reply: aiResult.reply, - proposedJourney: (aiResult.journey as JourneySimple) ?? null + proposedJourney: rawJourney != null ? sanitizeJourney(rawJourney) : null } } }) From 30eb96160db546fcb62776ad61a038ccaf742227 Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 18 Mar 2026 01:22:44 +0000 Subject: [PATCH 07/10] fix(ai-editor): show proposed journey in preview immediately on AI response --- .../pages/journeys/[journeyId]/ai.tsx | 21 +++++++++++++++---- .../src/components/AiEditor/AiChat/AiChat.tsx | 21 ++++++++++++------- .../AiEditor/AiEditorCardPreview.tsx | 4 ++-- .../components/AiEditor/AiEditorFlowMap.tsx | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx index 74638882b76..8cc5324a2f4 100644 --- a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx +++ b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx @@ -42,6 +42,9 @@ function AiEditorPage(): ReactElement { const [currentJourney, setCurrentJourney] = useState( null ) + const [proposedJourney, setProposedJourney] = useState( + null + ) const [aiState, setAiState] = useState({ status: 'idle', affectedCardIds: [] @@ -58,9 +61,11 @@ function AiEditorPage(): ReactElement { const journey = data?.journey + const previewJourney = proposedJourney ?? currentJourney + const selectedCardIndex = - currentJourney != null && selectedCardId != null - ? currentJourney.cards.findIndex((c) => c.id === selectedCardId) + 1 + previewJourney != null && selectedCardId != null + ? previewJourney.cards.findIndex((c) => c.id === selectedCardId) + 1 : null const handleJourneyUpdated = useCallback((updatedJourney: JourneySimple) => { @@ -68,6 +73,13 @@ function AiEditorPage(): ReactElement { setAiState({ status: 'idle', affectedCardIds: [] }) }, []) + const handleProposedJourney = useCallback( + (journey: JourneySimple | null) => { + setProposedJourney(journey) + }, + [] + ) + const handleSelectedCardChange = useCallback( (cardId: string | null) => { setSelectedCardId(cardId) @@ -127,6 +139,7 @@ function AiEditorPage(): ReactElement { } onClearSelectedCard={handleClearSelectedCard} onAiState={setAiState} + onProposedJourney={handleProposedJourney} onJourneyUpdated={handleJourneyUpdated} sx={{ width: { xs: '100%', md: '40%' }, @@ -136,9 +149,9 @@ function AiEditorPage(): ReactElement { } }} /> - {currentJourney != null ? ( + {previewJourney != null ? ( void onAiState: (state: AiState) => void + onProposedJourney: (journey: JourneySimple | null) => void onJourneyUpdated: (journey: JourneySimple) => void sx?: SxProps } @@ -207,6 +208,7 @@ export function AiChat({ selectedCardIndex, onClearSelectedCard, onAiState, + onProposedJourney, onJourneyUpdated, sx }: AiChatProps): ReactElement { @@ -273,11 +275,12 @@ export function AiChat({ dispatch({ type: 'RECEIVE', - reply: data.reply, + reply: data.reply ?? '', proposedJourney, diffSummary: diff.summary }) + onProposedJourney(proposedJourney) onAiState({ status: proposedJourney != null ? 'proposal' : 'idle', affectedCardIds: diff.affectedCardIds @@ -297,6 +300,7 @@ export function AiChat({ selectedCardId, currentJourney, journeyAiEdit, + onProposedJourney, onAiState ]) @@ -318,6 +322,7 @@ export function AiChat({ type: 'APPLY_SUCCESS', messageGenerationId: message.generationId }) + onProposedJourney(null) onJourneyUpdated(message.proposedJourney) onAiState({ status: 'idle', affectedCardIds: [] }) } catch { @@ -331,6 +336,7 @@ export function AiChat({ state.generationId, journeyId, journeySimpleUpdate, + onProposedJourney, onJourneyUpdated, onAiState ] @@ -339,9 +345,10 @@ export function AiChat({ const handleDismiss = useCallback( (message: ChatMessage) => { dispatch({ type: 'DISMISS', messageGenerationId: message.generationId }) + onProposedJourney(null) onAiState({ status: 'idle', affectedCardIds: [] }) }, - [onAiState] + [onProposedJourney, onAiState] ) const isEmpty = state.messages.length === 0 diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx index a5ccb650a39..e069220c519 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx @@ -1,6 +1,6 @@ import Box from '@mui/material/Box' -import Typography from '@mui/material/Typography' import { SxProps } from '@mui/material/styles' +import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement } from 'react' @@ -220,7 +220,7 @@ export function AiEditorCardPreview({ zIndex: 2 }} > - AI CHANGE + {t('AI CHANGE')} )} diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx index 0fb6ff0cdb9..1672fe26169 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorFlowMap.tsx @@ -1,8 +1,8 @@ import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' import Box from '@mui/material/Box' import Paper from '@mui/material/Paper' -import Typography from '@mui/material/Typography' import { SxProps } from '@mui/material/styles' +import Typography from '@mui/material/Typography' import { ReactElement } from 'react' import { JourneySimple, JourneySimpleCard } from '@core/shared/ai/journeySimpleTypes' From 510ad5ab7c3081f23951f9e5b1acdd5c662f0418 Mon Sep 17 00:00:00 2001 From: edmonday Date: Wed, 18 Mar 2026 02:08:56 +0000 Subject: [PATCH 08/10] feat(ai-editor): replace card preview with FramePortal+BlockRenderer, add journey load on mount - AiEditorCardPreview now uses useJourney() + transformer() + FramePortal + BlockRenderer to render actual card content instead of simplified HTML representation - AiEditorFlowMap rewritten to use React Flow for the journey flow map - ai.tsx loads JourneySimple on mount via journeySimpleGet query so the flow map is visible immediately without needing to interact with AI first - Added JourneyProvider wrapper around preview area with useJourneyQuery (full blocks) so card preview can render real block content - Refetch full journey after AI changes are applied Co-Authored-By: Claude Sonnet 4.6 --- .../GetJourneySimpleForAiEditor.ts | 16 + .../pages/journeys/[journeyId]/ai.tsx | 94 +++-- .../AiEditor/AiEditorCardPreview.tsx | 231 +++------- .../components/AiEditor/AiEditorFlowMap.tsx | 396 +++++++++--------- .../components/AiEditor/AiEditorPreview.tsx | 1 - 5 files changed, 345 insertions(+), 393 deletions(-) create mode 100644 apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts diff --git a/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts b/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts new file mode 100644 index 00000000000..233f2480465 --- /dev/null +++ b/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetJourneySimpleForAiEditor +// ==================================================== + +export interface GetJourneySimpleForAiEditor { + journeySimpleGet: Record | null; +} + +export interface GetJourneySimpleForAiEditorVariables { + id: string; +} diff --git a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx index 8cc5324a2f4..cb021fcfdee 100644 --- a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx +++ b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx @@ -6,14 +6,21 @@ import { GetServerSidePropsContext } from 'next' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' import { NextSeo } from 'next-seo' -import { ReactElement, useCallback, useState } from 'react' +import { ReactElement, useCallback, useEffect, useState } from 'react' +import { JourneyProvider } from '@core/journeys/ui/JourneyProvider' +import { useJourneyQuery } from '@core/journeys/ui/useJourneyQuery' import { JourneySimple } from '@core/shared/ai/journeySimpleTypes' import { GetAiEditorJourney, GetAiEditorJourneyVariables } from '../../../__generated__/GetAiEditorJourney' +import { + GetJourneySimpleForAiEditor, + GetJourneySimpleForAiEditorVariables +} from '../../../__generated__/GetJourneySimpleForAiEditor' +import { IdType } from '../../../__generated__/globalTypes' import { AiChat, AiState } from '../../../src/components/AiEditor/AiChat/AiChat' import { AiEditorHeader } from '../../../src/components/AiEditor/AiEditorHeader' import { AiEditorPreview } from '../../../src/components/AiEditor/AiEditorPreview' @@ -34,6 +41,12 @@ const GET_AI_EDITOR_JOURNEY = gql` } ` +const GET_JOURNEY_SIMPLE_FOR_AI_EDITOR = gql` + query GetJourneySimpleForAiEditor($id: ID!) { + journeySimpleGet(id: $id) + } +` + function AiEditorPage(): ReactElement { const { t } = useTranslation('apps-journeys-admin') const router = useRouter() @@ -59,6 +72,27 @@ function AiEditorPage(): ReactElement { skip: journeyId == null }) + const { data: simpleData } = useQuery< + GetJourneySimpleForAiEditor, + GetJourneySimpleForAiEditorVariables + >(GET_JOURNEY_SIMPLE_FOR_AI_EDITOR, { + variables: { id: journeyId }, + skip: journeyId == null + }) + + const { data: journeyData, refetch: refetchJourney } = useJourneyQuery({ + id: journeyId, + idType: IdType.databaseId, + options: { skipRoutingFilter: true } + }) + + // Populate currentJourney from the initial fetch + useEffect(() => { + if (simpleData?.journeySimpleGet != null && currentJourney == null) { + setCurrentJourney(simpleData.journeySimpleGet as JourneySimple) + } + }, [simpleData, currentJourney]) + const journey = data?.journey const previewJourney = proposedJourney ?? currentJourney @@ -68,10 +102,14 @@ function AiEditorPage(): ReactElement { ? previewJourney.cards.findIndex((c) => c.id === selectedCardId) + 1 : null - const handleJourneyUpdated = useCallback((updatedJourney: JourneySimple) => { - setCurrentJourney(updatedJourney) - setAiState({ status: 'idle', affectedCardIds: [] }) - }, []) + const handleJourneyUpdated = useCallback( + (updatedJourney: JourneySimple) => { + setCurrentJourney(updatedJourney) + setAiState({ status: 'idle', affectedCardIds: [] }) + void refetchJourney() + }, + [refetchJourney] + ) const handleProposedJourney = useCallback( (journey: JourneySimple | null) => { @@ -149,28 +187,30 @@ function AiEditorPage(): ReactElement { } }} /> - {previewJourney != null ? ( - - ) : ( - - - {t('Start chatting to see your journey preview')} - - - )} + + {previewJourney != null ? ( + + ) : ( + + + + )} + diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx index e069220c519..1002767291b 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorCardPreview.tsx @@ -2,187 +2,65 @@ import Box from '@mui/material/Box' import { SxProps } from '@mui/material/styles' import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' -import { ReactElement } from 'react' +import { ReactElement, useMemo } from 'react' -import { - JourneySimple, - JourneySimpleCard -} from '@core/shared/ai/journeySimpleTypes' +import { TreeBlock } from '@core/journeys/ui/block' +import { BlockRenderer } from '@core/journeys/ui/BlockRenderer' +import { FramePortal } from '@core/journeys/ui/FramePortal' +import { getStepTheme } from '@core/journeys/ui/getStepTheme' +import { useJourney } from '@core/journeys/ui/JourneyProvider' +import { getJourneyRTL } from '@core/journeys/ui/rtl' +import { StepFields } from '@core/journeys/ui/Step/__generated__/StepFields' +import { transformer } from '@core/journeys/ui/transformer' +import { ThemeProvider } from '@core/shared/ui/ThemeProvider' import { AiState } from './AiChat/AiChat' +// Card natural dimensions (same as Canvas.tsx) +const CARD_WIDTH = 324 +const CARD_HEIGHT = 674 + interface AiEditorCardPreviewProps { - journey: JourneySimple selectedCardId: string | null aiState: AiState sx?: SxProps } -function CardContent({ - card, - cardIndex -}: { - card: JourneySimpleCard - cardIndex: number -}): ReactElement { - const { t } = useTranslation('apps-journeys-admin') - - return ( - - {/* Background image overlay */} - {card.backgroundImage != null && ( - - )} - - - - {t('Screen {{n}}', { n: cardIndex + 1 })} - - - {card.video != null && ( - - - 🎬 {t('Video')} - - - )} - - {card.image != null && ( - - )} - - {card.heading != null && ( - - {card.heading} - - )} - - {card.text != null && ( - - {card.text} - - )} - - - - {card.button != null && ( - - {card.button.text} - - )} - - {card.poll != null && card.poll.length > 0 && ( - - {card.poll.map((option, i) => ( - - {option.text} - - ))} - - )} - - - ) -} - export function AiEditorCardPreview({ - journey, selectedCardId, aiState, sx }: AiEditorCardPreviewProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') - const cardIndex = journey.cards.findIndex((c) => c.id === selectedCardId) - const card = cardIndex >= 0 ? journey.cards[cardIndex] : null + const { journey } = useJourney() + + const steps = useMemo( + () => + transformer(journey?.blocks ?? []) as Array>, + [journey?.blocks] + ) + + // Map "card-N" → steps[N-1] + const selectedStep = useMemo(() => { + if (selectedCardId == null) return null + const match = /^card-(\d+)$/.exec(selectedCardId) + if (match == null) return null + const index = parseInt(match[1], 10) - 1 + return steps[index] ?? null + }, [selectedCardId, steps]) + + const { rtl, locale } = getJourneyRTL(journey) + + const theme = + selectedStep != null ? getStepTheme(selectedStep, journey) : null + const isAffected = selectedCardId != null && aiState.affectedCardIds.includes(selectedCardId) + // Scale the 324×674 card to fit the container + const scale = 0.42 + return ( - {card == null ? ( + {selectedStep == null || theme == null ? ( {t('Click a screen in the map below to preview it')} ) : ( - + + + {() => ( + + + + )} + + {isAffected && aiState.status === 'proposal' && ( ): ReactElement => { + const { label, cardIndex, isAffected, isLoading, isSelected } = data + const theme = useTheme() + + const borderColor = isSelected + ? theme.palette.primary.main + : isAffected + ? theme.palette.primary.main + : theme.palette.divider + + return ( + <> + + + + + {cardIndex} + + + + {label !== '' ? label : '…'} + + + + + ) + } +) +AiCardNode.displayName = 'AiCardNode' + interface AiEditorFlowMapProps { journey: JourneySimple selectedCardId: string | null @@ -17,219 +125,105 @@ interface AiEditorFlowMapProps { sx?: SxProps } -interface CardNode { - card: JourneySimpleCard - index: number -} - -function getCardConnections( - cards: JourneySimpleCard[] -): Array<{ from: string; to: string }> { - const connections: Array<{ from: string; to: string }> = [] - - for (const card of cards) { - if (card.defaultNextCard != null) { - connections.push({ from: card.id, to: card.defaultNextCard }) - } - if (card.button?.nextCard != null) { - connections.push({ from: card.id, to: card.button.nextCard }) - } - if (card.poll != null) { - for (const option of card.poll) { - if (option.nextCard != null) { - connections.push({ from: card.id, to: option.nextCard }) - } - } - } - } - - return connections -} - -export function AiEditorFlowMap({ +function AiEditorFlowMapInner({ journey, selectedCardId, aiState, onCardSelect, sx }: AiEditorFlowMapProps): ReactElement { - const CARD_WIDTH = 120 - const CARD_HEIGHT = 72 - const H_GAP = 48 - const V_GAP = 32 - const CARDS_PER_ROW = 3 - - const nodes: CardNode[] = journey.cards.map((card, index) => ({ - card, - index - })) - - const useStoredCoords = journey.cards.every( - (c) => typeof c.x === 'number' && typeof c.y === 'number' - ) - - function getPos(node: CardNode): { x: number; y: number } { - if (useStoredCoords) { - return { - x: node.card.x * (CARD_WIDTH + H_GAP), - y: node.card.y * (CARD_HEIGHT + V_GAP) + const nodeTypes = useMemo(() => ({ aiCard: AiCardNode }), []) + + const { nodes, edges } = useMemo(() => { + const nodes: Node[] = journey.cards.map((card, index) => ({ + id: card.id, + position: { x: card.x, y: card.y }, + type: 'aiCard', + data: { + cardIndex: index + 1, + label: card.heading ?? card.text ?? '', + isAffected: aiState.affectedCardIds.includes(card.id), + isLoading: aiState.status === 'loading', + isSelected: card.id === selectedCardId + } + })) + + const edges: Edge[] = [] + for (const card of journey.cards) { + if (card.defaultNextCard != null) { + edges.push({ + id: `${card.id}->default->${card.defaultNextCard}`, + source: card.id, + target: card.defaultNextCard, + style: { strokeWidth: 1.5 } + }) + } + if (card.button?.nextCard != null) { + edges.push({ + id: `${card.id}->btn->${card.button.nextCard}`, + source: card.id, + target: card.button.nextCard, + label: card.button.text, + style: { strokeWidth: 1.5 } + }) + } + if (card.poll != null) { + card.poll.forEach((opt, i) => { + if (opt.nextCard != null) { + edges.push({ + id: `${card.id}->poll${i}->${opt.nextCard}`, + source: card.id, + target: opt.nextCard, + label: opt.text, + style: { strokeWidth: 1.5 } + }) + } + }) } } - const col = node.index % CARDS_PER_ROW - const row = Math.floor(node.index / CARDS_PER_ROW) - return { - x: col * (CARD_WIDTH + H_GAP) + H_GAP, - y: row * (CARD_HEIGHT + V_GAP) + V_GAP - } - } - - const positions = new Map(nodes.map((n) => [n.card.id, getPos(n)])) - const maxX = - Math.max(...Array.from(positions.values()).map((p) => p.x)) + - CARD_WIDTH + - H_GAP - const maxY = - Math.max(...Array.from(positions.values()).map((p) => p.y)) + - CARD_HEIGHT + - V_GAP + return { nodes, edges } + }, [journey, selectedCardId, aiState]) - const connections = getCardConnections(journey.cards) + function handleNodeClick(_: MouseEvent, node: Node): void { + onCardSelect(node.id) + } return ( - - {/* SVG arrows */} - - {connections.map(({ from, to }, i) => { - const fromPos = positions.get(from) - const toPos = positions.get(to) - if (fromPos == null || toPos == null) return null - - const x1 = fromPos.x + CARD_WIDTH / 2 - const y1 = fromPos.y + CARD_HEIGHT - const x2 = toPos.x + CARD_WIDTH / 2 - const y2 = toPos.y - - return ( - - - - - - - - - ) - })} - - - {/* Card nodes */} - {nodes.map((node) => { - const pos = getPos(node) - const isSelected = selectedCardId === node.card.id - const isAffected = aiState.affectedCardIds.includes(node.card.id) - const isLoading = aiState.status === 'loading' && isAffected - const isProposal = aiState.status === 'proposal' && isAffected - - return ( - onCardSelect(node.card.id)} - data-testid={`FlowMapCard-${node.card.id}`} - sx={{ - position: 'absolute', - left: pos.x, - top: pos.y, - width: CARD_WIDTH, - height: CARD_HEIGHT, - p: 1, - cursor: 'pointer', - overflow: 'hidden', - border: 2, - borderColor: isSelected - ? 'primary.main' - : isProposal - ? 'primary.light' - : isLoading - ? 'warning.light' - : 'transparent', - animation: isLoading - ? 'pulse 1.5s ease-in-out infinite' - : 'none', - '@keyframes pulse': { - '0%, 100%': { opacity: 1 }, - '50%': { opacity: 0.6 } - }, - '&:hover': { borderColor: 'primary.light', elevation: 2 } - }} - > - - - {node.index + 1} - - - {node.card.heading ?? node.card.text ?? '(empty)'} - - {isProposal && ( - - )} - - - ) - })} - + + + ) } + +export function AiEditorFlowMap(props: AiEditorFlowMapProps): ReactElement { + return ( + + + + ) +} diff --git a/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx b/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx index b9b48921de5..4cf7316266a 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiEditorPreview.tsx @@ -40,7 +40,6 @@ export function AiEditorPreview({ }} > Date: Wed, 18 Mar 2026 02:13:06 +0000 Subject: [PATCH 09/10] fix: lint issues --- .../src/schema/journeyAiEdit/journeyAiEdit.ts | 31 ++++++++++++------- .../GetJourneySimpleForAiEditor.ts | 2 +- .../pages/journeys/[journeyId]/ai.tsx | 18 ++++------- .../src/components/AiEditor/AiChat/AiChat.tsx | 9 ++++-- .../AiEditor/AiChat/AiChatInput.tsx | 9 ++++-- .../AiEditor/AiChat/AiChatMessage.tsx | 4 +-- .../AiEditor/AiEditorCardPreview.tsx | 6 ++-- libs/locales/en/apps-journeys-admin.json | 24 +++++++++++++- .../gql/src/__generated__/graphql-env.d.ts | 5 ++- 9 files changed, 70 insertions(+), 38 deletions(-) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts index 5a468d01e5c..82dd1ac5bb1 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -1,15 +1,15 @@ import { google } from '@ai-sdk/google' -import { generateObject, ModelMessage, NoObjectGeneratedError } from 'ai' +import { ModelMessage, NoObjectGeneratedError, generateObject } from 'ai' import { GraphQLError } from 'graphql' import { z } from 'zod' import { prisma } from '@core/prisma/journeys/client' -import { hardenPrompt } from '@core/shared/ai/prompts' import { JourneySimple, JourneySimpleCard, journeySimpleSchema } from '@core/shared/ai/journeySimpleTypes' +import { hardenPrompt } from '@core/shared/ai/prompts' import { Action, ability, subject } from '../../lib/auth/ability' import { builder } from '../builder' @@ -23,8 +23,9 @@ interface JourneyAiEditResult { proposedJourney: JourneySimple | null } -const JourneyAiEditResultRef = - builder.objectRef('JourneyAiEditResult') +const JourneyAiEditResultRef = builder.objectRef( + 'JourneyAiEditResult' +) builder.objectType(JourneyAiEditResultRef, { fields: (t) => ({ @@ -122,7 +123,9 @@ builder.mutationField('journeyAiEdit', (t) => }) } - if (!ability(Action.Update, subject('Journey', dbJourney), context.user)) { + if ( + !ability(Action.Update, subject('Journey', dbJourney), context.user) + ) { throw new GraphQLError( 'user does not have permission to update journey', { extensions: { code: 'FORBIDDEN' } } @@ -136,15 +139,21 @@ builder.mutationField('journeyAiEdit', (t) => } catch (error) { if (error instanceof GraphQLError) throw error console.error('journeyAiEdit: failed to load journey', error) - throw new GraphQLError('Failed to load journey data. Please try again.', { - extensions: { code: 'INTERNAL_SERVER_ERROR' } - }) + throw new GraphQLError( + 'Failed to load journey data. Please try again.', + { + extensions: { code: 'INTERNAL_SERVER_ERROR' } + } + ) } // 4. Prune history to last 10 turns const prunedHistory = (input.history ?? []) .slice(-10) - .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content })) + .map((m) => ({ + role: m.role as 'user' | 'assistant', + content: m.content + })) // 5. Harden user message const hardenedMessage = hardenPrompt(input.message) @@ -164,7 +173,7 @@ builder.mutationField('journeyAiEdit', (t) => messages: [ ...prunedHistory, { role: 'user', content: hardenedMessage } - ] as ModelMessage[], + ], schema: journeyAiEditSchema, maxRetries: 2, abortSignal: AbortSignal.timeout(30_000) @@ -198,7 +207,7 @@ builder.mutationField('journeyAiEdit', (t) => }) // 9. Return result - const rawJourney = aiResult.journey as JourneySimple | null + const rawJourney = aiResult.journey return { reply: aiResult.reply, proposedJourney: rawJourney != null ? sanitizeJourney(rawJourney) : null diff --git a/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts b/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts index 233f2480465..798a6da2f3d 100644 --- a/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts +++ b/apps/journeys-admin/__generated__/GetJourneySimpleForAiEditor.ts @@ -8,7 +8,7 @@ // ==================================================== export interface GetJourneySimpleForAiEditor { - journeySimpleGet: Record | null; + journeySimpleGet: any | null; } export interface GetJourneySimpleForAiEditorVariables { diff --git a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx index cb021fcfdee..ed58c6234a1 100644 --- a/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx +++ b/apps/journeys-admin/pages/journeys/[journeyId]/ai.tsx @@ -111,19 +111,13 @@ function AiEditorPage(): ReactElement { [refetchJourney] ) - const handleProposedJourney = useCallback( - (journey: JourneySimple | null) => { - setProposedJourney(journey) - }, - [] - ) + const handleProposedJourney = useCallback((journey: JourneySimple | null) => { + setProposedJourney(journey) + }, []) - const handleSelectedCardChange = useCallback( - (cardId: string | null) => { - setSelectedCardId(cardId) - }, - [] - ) + const handleSelectedCardChange = useCallback((cardId: string | null) => { + setSelectedCardId(cardId) + }, []) const handleClearSelectedCard = useCallback(() => { setSelectedCardId(null) diff --git a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx index 03a0f8168be..8fcdd4ab276 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChat.tsx @@ -309,7 +309,10 @@ export function AiChat({ if (message.proposedJourney == null) return if (message.generationId !== state.generationId) return - dispatch({ type: 'APPLY_START', messageGenerationId: message.generationId }) + dispatch({ + type: 'APPLY_START', + messageGenerationId: message.generationId + }) try { await journeySimpleUpdate({ @@ -383,7 +386,9 @@ export function AiChat({ {isEmpty ? ( - + {t('Describe changes to your journey in plain language')} diff --git a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx index 57f7f52f9a4..f6683ce91aa 100644 --- a/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx +++ b/apps/journeys-admin/src/components/AiEditor/AiChat/AiChatInput.tsx @@ -50,9 +50,12 @@ export function AiChatInput({ - transformer(journey?.blocks ?? []) as Array>, + () => transformer(journey?.blocks ?? []) as Array>, [journey?.blocks] ) @@ -55,8 +54,7 @@ export function AiEditorCardPreview({ selectedStep != null ? getStepTheme(selectedStep, journey) : null const isAffected = - selectedCardId != null && - aiState.affectedCardIds.includes(selectedCardId) + selectedCardId != null && aiState.affectedCardIds.includes(selectedCardId) // Scale the 324×674 card to fit the container const scale = 0.42 diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json index 9d9d85e0737..100ce3ce9a2 100644 --- a/libs/locales/en/apps-journeys-admin.json +++ b/libs/locales/en/apps-journeys-admin.json @@ -11,6 +11,9 @@ "Request Access": "Request Access", "Edit {{title}}": "Edit {{title}}", "Edit Journey": "Edit Journey", + "AI Edit — {{title}}": "AI Edit — {{title}}", + "AI Editor": "AI Editor", + "Untitled Journey": "Untitled Journey", "{{title}} Express Setup": "{{title}} Express Setup", "Journey Express Setup": "Journey Express Setup", "Journey Analytics": "Journey Analytics", @@ -88,6 +91,24 @@ "Owner": "Owner", "Editor": "Editor", "Pending": "Pending", + "Describe changes to your journey in plain language": "Describe changes to your journey in plain language", + "Thinking...": "Thinking...", + "Screen {{n}} selected — changes will target this screen": "Screen {{n}} selected — changes will target this screen", + "Describe what you'd like to change...": "Describe what you'd like to change...", + "{{n}} characters remaining": "{{n}} characters remaining", + "AI Assistant": "AI Assistant", + "This proposal is outdated": "This proposal is outdated", + "Proposed changes": "Proposed changes", + "Applying...": "Applying...", + "Apply Changes": "Apply Changes", + "Dismiss": "Dismiss", + "✓ Applied": "✓ Applied", + "Dismissed": "Dismissed", + "Click a screen in the map below to preview it": "Click a screen in the map below to preview it", + "AI CHANGE": "AI CHANGE", + "Back to journeys": "Back to journeys", + "Switch to manual editor": "Switch to manual editor", + "Manual Editor": "Manual Editor", "User with Requested Access": "User with Requested Access", "Error loading report": "Error loading report", "There was an error loading the report": "There was an error loading the report", @@ -507,7 +528,6 @@ "Select a video block first": "Select a video block first", "Paste any YouTube Link": "Paste any YouTube Link", "Subtitles are available for this video": "Subtitles are available for this video", - "Dismiss": "Dismiss", "Available Languages": "Available Languages", "Apply": "Apply", "Video Library": "Video Library", @@ -596,6 +616,7 @@ "Edit Journey Actions": "Edit Journey Actions", "Back to Home": "Back to Home", "Click to edit": "Click to edit", + "Switch to AI Editor": "Switch to AI Editor", "Please verify your email": "Please verify your email", "You need to verify your email before you can continue.": "You need to verify your email before you can continue.", "Send verification email": "Send verification email", @@ -658,6 +679,7 @@ "Journey Archived": "Journey Archived", "Journey Unarchive failed": "Journey Unarchive failed", "Journey Unarchived": "Journey Unarchived", + "Edit with AI": "Edit with AI", "Access": "Access", "Translate": "Translate", "Journey Deleted": "Journey Deleted", diff --git a/libs/shared/gql/src/__generated__/graphql-env.d.ts b/libs/shared/gql/src/__generated__/graphql-env.d.ts index e57e0d97481..0ed4a0c28b4 100644 --- a/libs/shared/gql/src/__generated__/graphql-env.d.ts +++ b/libs/shared/gql/src/__generated__/graphql-env.d.ts @@ -117,6 +117,8 @@ export type introspection_types = { 'IntegrationGrowthSpacesUpdateInput': { kind: 'INPUT_OBJECT'; name: 'IntegrationGrowthSpacesUpdateInput'; isOneOf: false; inputFields: [{ name: 'accessId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'accessSecret'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'IntegrationType': { name: 'IntegrationType'; enumValues: 'google' | 'growthSpaces'; }; 'Journey': { kind: 'OBJECT'; name: 'Journey'; fields: { 'archivedAt': { name: 'archivedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'blockTypenames': { name: 'blockTypenames'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'blocks': { name: 'blocks'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; } }; 'chatButtons': { name: 'chatButtons'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; }; }; } }; 'createdAt': { name: 'createdAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; }; } }; 'creatorDescription': { name: 'creatorDescription'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'creatorImageBlock': { name: 'creatorImageBlock'; type: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; } }; 'customizable': { name: 'customizable'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'deletedAt': { name: 'deletedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'description': { name: 'description'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'displayTitle': { name: 'displayTitle'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'featuredAt': { name: 'featuredAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'fromTemplateId': { name: 'fromTemplateId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'host': { name: 'host'; type: { kind: 'OBJECT'; name: 'Host'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyCollections': { name: 'journeyCollections'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; }; }; } }; 'journeyCustomizationDescription': { name: 'journeyCustomizationDescription'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'journeyCustomizationFields': { name: 'journeyCustomizationFields'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyTheme': { name: 'journeyTheme'; type: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; } }; 'language': { name: 'language'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Language'; ofType: null; }; } }; 'languageId': { name: 'languageId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'logoImageBlock': { name: 'logoImageBlock'; type: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; } }; 'menuButtonIcon': { name: 'menuButtonIcon'; type: { kind: 'ENUM'; name: 'JourneyMenuButtonIcon'; ofType: null; } }; 'menuStepBlock': { name: 'menuStepBlock'; type: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; } }; 'plausibleToken': { name: 'plausibleToken'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'primaryImageBlock': { name: 'primaryImageBlock'; type: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; } }; 'publishedAt': { name: 'publishedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'seoDescription': { name: 'seoDescription'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'seoTitle': { name: 'seoTitle'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'showAssistant': { name: 'showAssistant'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showChatButtons': { name: 'showChatButtons'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showDislikeButton': { name: 'showDislikeButton'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showDisplayTitle': { name: 'showDisplayTitle'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showHosts': { name: 'showHosts'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showLikeButton': { name: 'showLikeButton'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showLogo': { name: 'showLogo'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showMenu': { name: 'showMenu'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showReactionButtons': { name: 'showReactionButtons'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'showShareButton': { name: 'showShareButton'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'slug': { name: 'slug'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'socialNodeX': { name: 'socialNodeX'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'socialNodeY': { name: 'socialNodeY'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'status': { name: 'status'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'JourneyStatus'; ofType: null; }; } }; 'strategySlug': { name: 'strategySlug'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'tags': { name: 'tags'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Tag'; ofType: null; }; }; }; } }; 'team': { name: 'team'; type: { kind: 'OBJECT'; name: 'Team'; ofType: null; } }; 'template': { name: 'template'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'templateSite': { name: 'templateSite'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'themeMode': { name: 'themeMode'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'ThemeMode'; ofType: null; }; } }; 'themeName': { name: 'themeName'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'ThemeName'; ofType: null; }; } }; 'title': { name: 'title'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'trashedAt': { name: 'trashedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'updatedAt': { name: 'updatedAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; }; } }; 'userJourneys': { name: 'userJourneys'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; }; } }; 'website': { name: 'website'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; }; }; + 'JourneyAiEditInput': { kind: 'INPUT_OBJECT'; name: 'JourneyAiEditInput'; isOneOf: false; inputFields: [{ name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'message'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'history'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INPUT_OBJECT'; name: 'MessageHistoryItem'; ofType: null; }; }; }; defaultValue: null }, { name: 'selectedCardId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; + 'JourneyAiEditResult': { kind: 'OBJECT'; name: 'JourneyAiEditResult'; fields: { 'proposedJourney': { name: 'proposedJourney'; type: { kind: 'SCALAR'; name: 'Json'; ofType: null; } }; 'reply': { name: 'reply'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'JourneyAiTranslateInput': { kind: 'INPUT_OBJECT'; name: 'JourneyAiTranslateInput'; isOneOf: false; inputFields: [{ name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'journeyLanguageName'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'textLanguageId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'textLanguageName'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'JourneyAiTranslateProgress': { kind: 'OBJECT'; name: 'JourneyAiTranslateProgress'; fields: { 'journey': { name: 'journey'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'message': { name: 'message'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'progress': { name: 'progress'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; }; }; 'JourneyCollection': { kind: 'OBJECT'; name: 'JourneyCollection'; fields: { 'customDomains': { name: 'customDomains'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeys': { name: 'journeys'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; }; } }; 'team': { name: 'team'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'title': { name: 'title'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; @@ -172,6 +174,7 @@ export type introspection_types = { 'MeInput': { kind: 'INPUT_OBJECT'; name: 'MeInput'; isOneOf: false; inputFields: [{ name: 'redirect'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'app'; type: { kind: 'ENUM'; name: 'App'; ofType: null; }; defaultValue: null }]; }; 'MediaRole': { name: 'MediaRole'; enumValues: 'publisher'; }; 'MediaVideo': { kind: 'UNION'; name: 'MediaVideo'; fields: {}; possibleTypes: 'MuxVideo' | 'Video' | 'YouTube'; }; + 'MessageHistoryItem': { kind: 'INPUT_OBJECT'; name: 'MessageHistoryItem'; isOneOf: false; inputFields: [{ name: 'role'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'content'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'MessagePlatform': { name: 'MessagePlatform'; enumValues: 'facebook' | 'telegram' | 'whatsApp' | 'instagram' | 'kakaoTalk' | 'viber' | 'vk' | 'snapchat' | 'skype' | 'line' | 'tikTok' | 'custom' | 'globe2' | 'globe3' | 'messageText1' | 'messageText2' | 'send1' | 'send2' | 'messageChat2' | 'messageCircle' | 'messageNotifyCircle' | 'messageNotifySquare' | 'messageSquare' | 'mail1' | 'linkExternal' | 'home3' | 'home4' | 'helpCircleContained' | 'helpSquareContained' | 'shieldCheck' | 'menu1' | 'checkBroken' | 'checkContained' | 'settings' | 'discord' | 'signal' | 'weChat'; }; 'MultiselectBlock': { kind: 'OBJECT'; name: 'MultiselectBlock'; fields: { 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyId': { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'max': { name: 'max'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'min': { name: 'min'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'parentBlockId': { name: 'parentBlockId'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; } }; 'parentOrder': { name: 'parentOrder'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; }; }; 'MultiselectBlockCreateInput': { kind: 'INPUT_OBJECT'; name: 'MultiselectBlockCreateInput'; isOneOf: false; inputFields: [{ name: 'id'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'parentBlockId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }]; }; @@ -181,7 +184,7 @@ export type introspection_types = { 'MultiselectOptionBlockUpdateInput': { kind: 'INPUT_OBJECT'; name: 'MultiselectOptionBlockUpdateInput'; isOneOf: false; inputFields: [{ name: 'parentBlockId'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'MultiselectSubmissionEvent': { kind: 'OBJECT'; name: 'MultiselectSubmissionEvent'; fields: { 'createdAt': { name: 'createdAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyId': { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'label': { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'value': { name: 'value'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'MultiselectSubmissionEventCreateInput': { kind: 'INPUT_OBJECT'; name: 'MultiselectSubmissionEventCreateInput'; isOneOf: false; inputFields: [{ name: 'id'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'blockId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'stepId'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'values'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; }; defaultValue: null }]; }; - 'Mutation': { kind: 'OBJECT'; name: 'Mutation'; fields: { 'audioPreviewCreate': { name: 'audioPreviewCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewDelete': { name: 'audioPreviewDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewUpdate': { name: 'audioPreviewUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'bibleCitationCreate': { name: 'bibleCitationCreate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'bibleCitationDelete': { name: 'bibleCitationDelete'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'bibleCitationUpdate': { name: 'bibleCitationUpdate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'blockDelete': { name: 'blockDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockDeleteAction': { name: 'blockDeleteAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; } }; 'blockDuplicate': { name: 'blockDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockOrderUpdate': { name: 'blockOrderUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockRestore': { name: 'blockRestore'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockUpdateAction': { name: 'blockUpdateAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Action'; ofType: null; }; } }; 'blockUpdateChatAction': { name: 'blockUpdateChatAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatAction'; ofType: null; }; } }; 'blockUpdateEmailAction': { name: 'blockUpdateEmailAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'EmailAction'; ofType: null; }; } }; 'blockUpdateLinkAction': { name: 'blockUpdateLinkAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'LinkAction'; ofType: null; }; } }; 'blockUpdateNavigateToBlockAction': { name: 'blockUpdateNavigateToBlockAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'NavigateToBlockAction'; ofType: null; }; } }; 'blockUpdatePhoneAction': { name: 'blockUpdatePhoneAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'PhoneAction'; ofType: null; }; } }; 'buttonBlockCreate': { name: 'buttonBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; }; } }; 'buttonBlockUpdate': { name: 'buttonBlockUpdate'; type: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; } }; 'buttonClickEventCreate': { name: 'buttonClickEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonClickEvent'; ofType: null; }; } }; 'cardBlockCreate': { name: 'cardBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'cardBlockUpdate': { name: 'cardBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'chatButtonCreate': { name: 'chatButtonCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonRemove': { name: 'chatButtonRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonUpdate': { name: 'chatButtonUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatOpenEventCreate': { name: 'chatOpenEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatOpenEvent'; ofType: null; }; } }; 'cloudflareR2CompleteMultipart': { name: 'cloudflareR2CompleteMultipart'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Create': { name: 'cloudflareR2Create'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Delete': { name: 'cloudflareR2Delete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2MultipartPrepare': { name: 'cloudflareR2MultipartPrepare'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2MultipartPrepared'; ofType: null; }; } }; 'cloudflareUploadComplete': { name: 'cloudflareUploadComplete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'createCloudflareImageFromPrompt': { name: 'createCloudflareImageFromPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByFile': { name: 'createCloudflareUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByUrl': { name: 'createCloudflareUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createImageBySegmindPrompt': { name: 'createImageBySegmindPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createJourneyEventsExportLog': { name: 'createJourneyEventsExportLog'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyEventsExportLog'; ofType: null; }; } }; 'createKeyword': { name: 'createKeyword'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Keyword'; ofType: null; }; } }; 'createMuxVideoAndQueueUpload': { name: 'createMuxVideoAndQueueUpload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByFile': { name: 'createMuxVideoUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByUrl': { name: 'createMuxVideoUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createVerificationRequest': { name: 'createVerificationRequest'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'customDomainCheck': { name: 'customDomainCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomainCheck'; ofType: null; }; } }; 'customDomainCreate': { name: 'customDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainDelete': { name: 'customDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainUpdate': { name: 'customDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'deleteCloudflareImage': { name: 'deleteCloudflareImage'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'deleteMuxVideo': { name: 'deleteMuxVideo'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'enableMuxDownload': { name: 'enableMuxDownload'; type: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; } }; 'fixVideoLanguages': { name: 'fixVideoLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'googleSheetsSyncBackfill': { name: 'googleSheetsSyncBackfill'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncCreate': { name: 'googleSheetsSyncCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncDelete': { name: 'googleSheetsSyncDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'hostCreate': { name: 'hostCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostDelete': { name: 'hostDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostUpdate': { name: 'hostUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'iconBlockCreate': { name: 'iconBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'iconBlockUpdate': { name: 'iconBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'imageBlockCreate': { name: 'imageBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'imageBlockUpdate': { name: 'imageBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'integrationDelete': { name: 'integrationDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Integration'; ofType: null; }; } }; 'integrationGoogleCreate': { name: 'integrationGoogleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGoogleUpdate': { name: 'integrationGoogleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGrowthSpacesCreate': { name: 'integrationGrowthSpacesCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'integrationGrowthSpacesUpdate': { name: 'integrationGrowthSpacesUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'journeyAiTranslateCreate': { name: 'journeyAiTranslateCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCollectionCreate': { name: 'journeyCollectionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionDelete': { name: 'journeyCollectionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionUpdate': { name: 'journeyCollectionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCreate': { name: 'journeyCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCustomizationFieldPublisherUpdate': { name: 'journeyCustomizationFieldPublisherUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyCustomizationFieldUserUpdate': { name: 'journeyCustomizationFieldUserUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyDuplicate': { name: 'journeyDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyFeature': { name: 'journeyFeature'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeyLanguageAiDetect': { name: 'journeyLanguageAiDetect'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'journeyNotificationUpdate': { name: 'journeyNotificationUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyNotification'; ofType: null; }; } }; 'journeyProfileCreate': { name: 'journeyProfileCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyProfileUpdate': { name: 'journeyProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyPublish': { name: 'journeyPublish'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeySimpleUpdate': { name: 'journeySimpleUpdate'; type: { kind: 'SCALAR'; name: 'Json'; ofType: null; } }; 'journeyTemplate': { name: 'journeyTemplate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyThemeCreate': { name: 'journeyThemeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeDelete': { name: 'journeyThemeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeUpdate': { name: 'journeyThemeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyUpdate': { name: 'journeyUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyViewEventCreate': { name: 'journeyViewEventCreate'; type: { kind: 'OBJECT'; name: 'JourneyViewEvent'; ofType: null; } }; 'journeyVisitorExportToGoogleSheet': { name: 'journeyVisitorExportToGoogleSheet'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyVisitorGoogleSheetExportResult'; ofType: null; }; } }; 'journeysArchive': { name: 'journeysArchive'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysDelete': { name: 'journeysDelete'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysRestore': { name: 'journeysRestore'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysTrash': { name: 'journeysTrash'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'multiselectBlockCreate': { name: 'multiselectBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectBlockUpdate': { name: 'multiselectBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectOptionBlockCreate': { name: 'multiselectOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectOptionBlockUpdate': { name: 'multiselectOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectSubmissionEventCreate': { name: 'multiselectSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectSubmissionEvent'; ofType: null; }; } }; 'playlistCreate': { name: 'playlistCreate'; type: { kind: 'UNION'; name: 'MutationPlaylistCreateResult'; ofType: null; } }; 'playlistDelete': { name: 'playlistDelete'; type: { kind: 'UNION'; name: 'MutationPlaylistDeleteResult'; ofType: null; } }; 'playlistItemAdd': { name: 'playlistItemAdd'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddResult'; ofType: null; } }; 'playlistItemAddWithVideoAndLanguageIds': { name: 'playlistItemAddWithVideoAndLanguageIds'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddWithVideoAndLanguageIdsResult'; ofType: null; } }; 'playlistItemRemove': { name: 'playlistItemRemove'; type: { kind: 'UNION'; name: 'MutationPlaylistItemRemoveResult'; ofType: null; } }; 'playlistItemsReorder': { name: 'playlistItemsReorder'; type: { kind: 'UNION'; name: 'MutationPlaylistItemsReorderResult'; ofType: null; } }; 'playlistUpdate': { name: 'playlistUpdate'; type: { kind: 'UNION'; name: 'MutationPlaylistUpdateResult'; ofType: null; } }; 'qrCodeCreate': { name: 'qrCodeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeDelete': { name: 'qrCodeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeUpdate': { name: 'qrCodeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'radioOptionBlockCreate': { name: 'radioOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioOptionBlockUpdate': { name: 'radioOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioQuestionBlockCreate': { name: 'radioQuestionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionBlockUpdate': { name: 'radioQuestionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionSubmissionEventCreate': { name: 'radioQuestionSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionSubmissionEvent'; ofType: null; }; } }; 'shortLinkCreate': { name: 'shortLinkCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkCreateResult'; ofType: null; }; } }; 'shortLinkDelete': { name: 'shortLinkDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDeleteResult'; ofType: null; }; } }; 'shortLinkDomainCreate': { name: 'shortLinkDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainCreateResult'; ofType: null; }; } }; 'shortLinkDomainDelete': { name: 'shortLinkDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainDeleteResult'; ofType: null; }; } }; 'shortLinkDomainUpdate': { name: 'shortLinkDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainUpdateResult'; ofType: null; }; } }; 'shortLinkUpdate': { name: 'shortLinkUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkUpdateResult'; ofType: null; }; } }; 'signUpBlockCreate': { name: 'signUpBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; }; } }; 'signUpBlockUpdate': { name: 'signUpBlockUpdate'; type: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; } }; 'signUpSubmissionEventCreate': { name: 'signUpSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpSubmissionEvent'; ofType: null; }; } }; 'siteCreate': { name: 'siteCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationSiteCreateResult'; ofType: null; }; } }; 'spacerBlockCreate': { name: 'spacerBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'spacerBlockUpdate': { name: 'spacerBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'stepBlockCreate': { name: 'stepBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepBlockPositionUpdate': { name: 'stepBlockPositionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; }; }; } }; 'stepBlockUpdate': { name: 'stepBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepNextEventCreate': { name: 'stepNextEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepNextEvent'; ofType: null; }; } }; 'stepPreviousEventCreate': { name: 'stepPreviousEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepPreviousEvent'; ofType: null; }; } }; 'stepViewEventCreate': { name: 'stepViewEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepViewEvent'; ofType: null; }; } }; 'teamCreate': { name: 'teamCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'teamUpdate': { name: 'teamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'textResponseBlockCreate': { name: 'textResponseBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; }; } }; 'textResponseBlockUpdate': { name: 'textResponseBlockUpdate'; type: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; } }; 'textResponseSubmissionEventCreate': { name: 'textResponseSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseSubmissionEvent'; ofType: null; }; } }; 'triggerUnsplashDownload': { name: 'triggerUnsplashDownload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'typographyBlockCreate': { name: 'typographyBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'typographyBlockUpdate': { name: 'typographyBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'updateJourneysEmailPreference': { name: 'updateJourneysEmailPreference'; type: { kind: 'OBJECT'; name: 'JourneysEmailPreference'; ofType: null; } }; 'updateVideoAlgoliaIndex': { name: 'updateVideoAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'updateVideoVariantAlgoliaIndex': { name: 'updateVideoVariantAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'userImpersonate': { name: 'userImpersonate'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'userInviteAcceptAll': { name: 'userInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; }; }; } }; 'userInviteCreate': { name: 'userInviteCreate'; type: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; } }; 'userInviteRemove': { name: 'userInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; } }; 'userJourneyApprove': { name: 'userJourneyApprove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyOpen': { name: 'userJourneyOpen'; type: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; } }; 'userJourneyPromote': { name: 'userJourneyPromote'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemove': { name: 'userJourneyRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemoveAll': { name: 'userJourneyRemoveAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; }; }; } }; 'userJourneyRequest': { name: 'userJourneyRequest'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userMediaProfileUpdate': { name: 'userMediaProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserMediaProfile'; ofType: null; }; } }; 'userTeamDelete': { name: 'userTeamDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'userTeamInviteAcceptAll': { name: 'userTeamInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; }; }; } }; 'userTeamInviteCreate': { name: 'userTeamInviteCreate'; type: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; } }; 'userTeamInviteRemove': { name: 'userTeamInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; } }; 'userTeamUpdate': { name: 'userTeamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'validateEmail': { name: 'validateEmail'; type: { kind: 'OBJECT'; name: 'AuthenticatedUser'; ofType: null; } }; 'videoBlockCreate': { name: 'videoBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoBlockUpdate': { name: 'videoBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoCollapseEventCreate': { name: 'videoCollapseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCollapseEvent'; ofType: null; }; } }; 'videoCompleteEventCreate': { name: 'videoCompleteEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCompleteEvent'; ofType: null; }; } }; 'videoCreate': { name: 'videoCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDelete': { name: 'videoDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDescriptionCreate': { name: 'videoDescriptionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionDelete': { name: 'videoDescriptionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionUpdate': { name: 'videoDescriptionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoEditionCreate': { name: 'videoEditionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionDelete': { name: 'videoEditionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionUpdate': { name: 'videoEditionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoExpandEventCreate': { name: 'videoExpandEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoExpandEvent'; ofType: null; }; } }; 'videoImageAltCreate': { name: 'videoImageAltCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltDelete': { name: 'videoImageAltDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltUpdate': { name: 'videoImageAltUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoOriginCreate': { name: 'videoOriginCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginDelete': { name: 'videoOriginDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginUpdate': { name: 'videoOriginUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoPauseEventCreate': { name: 'videoPauseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPauseEvent'; ofType: null; }; } }; 'videoPlayEventCreate': { name: 'videoPlayEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPlayEvent'; ofType: null; }; } }; 'videoProgressEventCreate': { name: 'videoProgressEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoProgressEvent'; ofType: null; }; } }; 'videoPublishChildren': { name: 'videoPublishChildren'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenResult'; ofType: null; }; } }; 'videoPublishChildrenAndLanguages': { name: 'videoPublishChildrenAndLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenAndLanguagesResult'; ofType: null; }; } }; 'videoSnippetCreate': { name: 'videoSnippetCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetDelete': { name: 'videoSnippetDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetUpdate': { name: 'videoSnippetUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoStartEventCreate': { name: 'videoStartEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStartEvent'; ofType: null; }; } }; 'videoStudyQuestionCreate': { name: 'videoStudyQuestionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionDelete': { name: 'videoStudyQuestionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionUpdate': { name: 'videoStudyQuestionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoSubtitleCreate': { name: 'videoSubtitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleDelete': { name: 'videoSubtitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleUpdate': { name: 'videoSubtitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoTitleCreate': { name: 'videoTitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleDelete': { name: 'videoTitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleUpdate': { name: 'videoTitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoUpdate': { name: 'videoUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoVariantCreate': { name: 'videoVariantCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDelete': { name: 'videoVariantDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDownloadCreate': { name: 'videoVariantDownloadCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadDelete': { name: 'videoVariantDownloadDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadUpdate': { name: 'videoVariantDownloadUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantUpdate': { name: 'videoVariantUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'visitorUpdate': { name: 'visitorUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; 'visitorUpdateForCurrentUser': { name: 'visitorUpdateForCurrentUser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; }; }; + 'Mutation': { kind: 'OBJECT'; name: 'Mutation'; fields: { 'audioPreviewCreate': { name: 'audioPreviewCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewDelete': { name: 'audioPreviewDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewUpdate': { name: 'audioPreviewUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'bibleCitationCreate': { name: 'bibleCitationCreate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'bibleCitationDelete': { name: 'bibleCitationDelete'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'bibleCitationUpdate': { name: 'bibleCitationUpdate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'blockDelete': { name: 'blockDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockDeleteAction': { name: 'blockDeleteAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; } }; 'blockDuplicate': { name: 'blockDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockOrderUpdate': { name: 'blockOrderUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockRestore': { name: 'blockRestore'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockUpdateAction': { name: 'blockUpdateAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Action'; ofType: null; }; } }; 'blockUpdateChatAction': { name: 'blockUpdateChatAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatAction'; ofType: null; }; } }; 'blockUpdateEmailAction': { name: 'blockUpdateEmailAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'EmailAction'; ofType: null; }; } }; 'blockUpdateLinkAction': { name: 'blockUpdateLinkAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'LinkAction'; ofType: null; }; } }; 'blockUpdateNavigateToBlockAction': { name: 'blockUpdateNavigateToBlockAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'NavigateToBlockAction'; ofType: null; }; } }; 'blockUpdatePhoneAction': { name: 'blockUpdatePhoneAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'PhoneAction'; ofType: null; }; } }; 'buttonBlockCreate': { name: 'buttonBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; }; } }; 'buttonBlockUpdate': { name: 'buttonBlockUpdate'; type: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; } }; 'buttonClickEventCreate': { name: 'buttonClickEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonClickEvent'; ofType: null; }; } }; 'cardBlockCreate': { name: 'cardBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'cardBlockUpdate': { name: 'cardBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'chatButtonCreate': { name: 'chatButtonCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonRemove': { name: 'chatButtonRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonUpdate': { name: 'chatButtonUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatOpenEventCreate': { name: 'chatOpenEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatOpenEvent'; ofType: null; }; } }; 'cloudflareR2CompleteMultipart': { name: 'cloudflareR2CompleteMultipart'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Create': { name: 'cloudflareR2Create'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Delete': { name: 'cloudflareR2Delete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2MultipartPrepare': { name: 'cloudflareR2MultipartPrepare'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2MultipartPrepared'; ofType: null; }; } }; 'cloudflareUploadComplete': { name: 'cloudflareUploadComplete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'createCloudflareImageFromPrompt': { name: 'createCloudflareImageFromPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByFile': { name: 'createCloudflareUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByUrl': { name: 'createCloudflareUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createImageBySegmindPrompt': { name: 'createImageBySegmindPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createJourneyEventsExportLog': { name: 'createJourneyEventsExportLog'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyEventsExportLog'; ofType: null; }; } }; 'createKeyword': { name: 'createKeyword'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Keyword'; ofType: null; }; } }; 'createMuxVideoAndQueueUpload': { name: 'createMuxVideoAndQueueUpload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByFile': { name: 'createMuxVideoUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByUrl': { name: 'createMuxVideoUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createVerificationRequest': { name: 'createVerificationRequest'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'customDomainCheck': { name: 'customDomainCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomainCheck'; ofType: null; }; } }; 'customDomainCreate': { name: 'customDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainDelete': { name: 'customDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainUpdate': { name: 'customDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'deleteCloudflareImage': { name: 'deleteCloudflareImage'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'deleteMuxVideo': { name: 'deleteMuxVideo'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'enableMuxDownload': { name: 'enableMuxDownload'; type: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; } }; 'fixVideoLanguages': { name: 'fixVideoLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'googleSheetsSyncBackfill': { name: 'googleSheetsSyncBackfill'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncCreate': { name: 'googleSheetsSyncCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncDelete': { name: 'googleSheetsSyncDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'hostCreate': { name: 'hostCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostDelete': { name: 'hostDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostUpdate': { name: 'hostUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'iconBlockCreate': { name: 'iconBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'iconBlockUpdate': { name: 'iconBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'imageBlockCreate': { name: 'imageBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'imageBlockUpdate': { name: 'imageBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'integrationDelete': { name: 'integrationDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Integration'; ofType: null; }; } }; 'integrationGoogleCreate': { name: 'integrationGoogleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGoogleUpdate': { name: 'integrationGoogleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGrowthSpacesCreate': { name: 'integrationGrowthSpacesCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'integrationGrowthSpacesUpdate': { name: 'integrationGrowthSpacesUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'journeyAiEdit': { name: 'journeyAiEdit'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyAiEditResult'; ofType: null; }; } }; 'journeyAiTranslateCreate': { name: 'journeyAiTranslateCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCollectionCreate': { name: 'journeyCollectionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionDelete': { name: 'journeyCollectionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionUpdate': { name: 'journeyCollectionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCreate': { name: 'journeyCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCustomizationFieldPublisherUpdate': { name: 'journeyCustomizationFieldPublisherUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyCustomizationFieldUserUpdate': { name: 'journeyCustomizationFieldUserUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyDuplicate': { name: 'journeyDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyFeature': { name: 'journeyFeature'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeyLanguageAiDetect': { name: 'journeyLanguageAiDetect'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'journeyNotificationUpdate': { name: 'journeyNotificationUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyNotification'; ofType: null; }; } }; 'journeyProfileCreate': { name: 'journeyProfileCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyProfileUpdate': { name: 'journeyProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyPublish': { name: 'journeyPublish'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeySimpleUpdate': { name: 'journeySimpleUpdate'; type: { kind: 'SCALAR'; name: 'Json'; ofType: null; } }; 'journeyTemplate': { name: 'journeyTemplate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyThemeCreate': { name: 'journeyThemeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeDelete': { name: 'journeyThemeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeUpdate': { name: 'journeyThemeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyUpdate': { name: 'journeyUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyViewEventCreate': { name: 'journeyViewEventCreate'; type: { kind: 'OBJECT'; name: 'JourneyViewEvent'; ofType: null; } }; 'journeyVisitorExportToGoogleSheet': { name: 'journeyVisitorExportToGoogleSheet'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyVisitorGoogleSheetExportResult'; ofType: null; }; } }; 'journeysArchive': { name: 'journeysArchive'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysDelete': { name: 'journeysDelete'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysRestore': { name: 'journeysRestore'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysTrash': { name: 'journeysTrash'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'multiselectBlockCreate': { name: 'multiselectBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectBlockUpdate': { name: 'multiselectBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectOptionBlockCreate': { name: 'multiselectOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectOptionBlockUpdate': { name: 'multiselectOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectSubmissionEventCreate': { name: 'multiselectSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectSubmissionEvent'; ofType: null; }; } }; 'playlistCreate': { name: 'playlistCreate'; type: { kind: 'UNION'; name: 'MutationPlaylistCreateResult'; ofType: null; } }; 'playlistDelete': { name: 'playlistDelete'; type: { kind: 'UNION'; name: 'MutationPlaylistDeleteResult'; ofType: null; } }; 'playlistItemAdd': { name: 'playlistItemAdd'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddResult'; ofType: null; } }; 'playlistItemAddWithVideoAndLanguageIds': { name: 'playlistItemAddWithVideoAndLanguageIds'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddWithVideoAndLanguageIdsResult'; ofType: null; } }; 'playlistItemRemove': { name: 'playlistItemRemove'; type: { kind: 'UNION'; name: 'MutationPlaylistItemRemoveResult'; ofType: null; } }; 'playlistItemsReorder': { name: 'playlistItemsReorder'; type: { kind: 'UNION'; name: 'MutationPlaylistItemsReorderResult'; ofType: null; } }; 'playlistUpdate': { name: 'playlistUpdate'; type: { kind: 'UNION'; name: 'MutationPlaylistUpdateResult'; ofType: null; } }; 'qrCodeCreate': { name: 'qrCodeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeDelete': { name: 'qrCodeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeUpdate': { name: 'qrCodeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'radioOptionBlockCreate': { name: 'radioOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioOptionBlockUpdate': { name: 'radioOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioQuestionBlockCreate': { name: 'radioQuestionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionBlockUpdate': { name: 'radioQuestionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionSubmissionEventCreate': { name: 'radioQuestionSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionSubmissionEvent'; ofType: null; }; } }; 'shortLinkCreate': { name: 'shortLinkCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkCreateResult'; ofType: null; }; } }; 'shortLinkDelete': { name: 'shortLinkDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDeleteResult'; ofType: null; }; } }; 'shortLinkDomainCreate': { name: 'shortLinkDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainCreateResult'; ofType: null; }; } }; 'shortLinkDomainDelete': { name: 'shortLinkDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainDeleteResult'; ofType: null; }; } }; 'shortLinkDomainUpdate': { name: 'shortLinkDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainUpdateResult'; ofType: null; }; } }; 'shortLinkUpdate': { name: 'shortLinkUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkUpdateResult'; ofType: null; }; } }; 'signUpBlockCreate': { name: 'signUpBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; }; } }; 'signUpBlockUpdate': { name: 'signUpBlockUpdate'; type: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; } }; 'signUpSubmissionEventCreate': { name: 'signUpSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpSubmissionEvent'; ofType: null; }; } }; 'siteCreate': { name: 'siteCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationSiteCreateResult'; ofType: null; }; } }; 'spacerBlockCreate': { name: 'spacerBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'spacerBlockUpdate': { name: 'spacerBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'stepBlockCreate': { name: 'stepBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepBlockPositionUpdate': { name: 'stepBlockPositionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; }; }; } }; 'stepBlockUpdate': { name: 'stepBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepNextEventCreate': { name: 'stepNextEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepNextEvent'; ofType: null; }; } }; 'stepPreviousEventCreate': { name: 'stepPreviousEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepPreviousEvent'; ofType: null; }; } }; 'stepViewEventCreate': { name: 'stepViewEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepViewEvent'; ofType: null; }; } }; 'teamCreate': { name: 'teamCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'teamUpdate': { name: 'teamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'textResponseBlockCreate': { name: 'textResponseBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; }; } }; 'textResponseBlockUpdate': { name: 'textResponseBlockUpdate'; type: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; } }; 'textResponseSubmissionEventCreate': { name: 'textResponseSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseSubmissionEvent'; ofType: null; }; } }; 'triggerUnsplashDownload': { name: 'triggerUnsplashDownload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'typographyBlockCreate': { name: 'typographyBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'typographyBlockUpdate': { name: 'typographyBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'updateJourneysEmailPreference': { name: 'updateJourneysEmailPreference'; type: { kind: 'OBJECT'; name: 'JourneysEmailPreference'; ofType: null; } }; 'updateVideoAlgoliaIndex': { name: 'updateVideoAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'updateVideoVariantAlgoliaIndex': { name: 'updateVideoVariantAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'userImpersonate': { name: 'userImpersonate'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'userInviteAcceptAll': { name: 'userInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; }; }; } }; 'userInviteCreate': { name: 'userInviteCreate'; type: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; } }; 'userInviteRemove': { name: 'userInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; } }; 'userJourneyApprove': { name: 'userJourneyApprove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyOpen': { name: 'userJourneyOpen'; type: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; } }; 'userJourneyPromote': { name: 'userJourneyPromote'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemove': { name: 'userJourneyRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemoveAll': { name: 'userJourneyRemoveAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; }; }; } }; 'userJourneyRequest': { name: 'userJourneyRequest'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userMediaProfileUpdate': { name: 'userMediaProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserMediaProfile'; ofType: null; }; } }; 'userTeamDelete': { name: 'userTeamDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'userTeamInviteAcceptAll': { name: 'userTeamInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; }; }; } }; 'userTeamInviteCreate': { name: 'userTeamInviteCreate'; type: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; } }; 'userTeamInviteRemove': { name: 'userTeamInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; } }; 'userTeamUpdate': { name: 'userTeamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'validateEmail': { name: 'validateEmail'; type: { kind: 'OBJECT'; name: 'AuthenticatedUser'; ofType: null; } }; 'videoBlockCreate': { name: 'videoBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoBlockUpdate': { name: 'videoBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoCollapseEventCreate': { name: 'videoCollapseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCollapseEvent'; ofType: null; }; } }; 'videoCompleteEventCreate': { name: 'videoCompleteEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCompleteEvent'; ofType: null; }; } }; 'videoCreate': { name: 'videoCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDelete': { name: 'videoDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDescriptionCreate': { name: 'videoDescriptionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionDelete': { name: 'videoDescriptionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionUpdate': { name: 'videoDescriptionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoEditionCreate': { name: 'videoEditionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionDelete': { name: 'videoEditionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionUpdate': { name: 'videoEditionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoExpandEventCreate': { name: 'videoExpandEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoExpandEvent'; ofType: null; }; } }; 'videoImageAltCreate': { name: 'videoImageAltCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltDelete': { name: 'videoImageAltDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltUpdate': { name: 'videoImageAltUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoOriginCreate': { name: 'videoOriginCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginDelete': { name: 'videoOriginDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginUpdate': { name: 'videoOriginUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoPauseEventCreate': { name: 'videoPauseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPauseEvent'; ofType: null; }; } }; 'videoPlayEventCreate': { name: 'videoPlayEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPlayEvent'; ofType: null; }; } }; 'videoProgressEventCreate': { name: 'videoProgressEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoProgressEvent'; ofType: null; }; } }; 'videoPublishChildren': { name: 'videoPublishChildren'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenResult'; ofType: null; }; } }; 'videoPublishChildrenAndLanguages': { name: 'videoPublishChildrenAndLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenAndLanguagesResult'; ofType: null; }; } }; 'videoSnippetCreate': { name: 'videoSnippetCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetDelete': { name: 'videoSnippetDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetUpdate': { name: 'videoSnippetUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoStartEventCreate': { name: 'videoStartEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStartEvent'; ofType: null; }; } }; 'videoStudyQuestionCreate': { name: 'videoStudyQuestionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionDelete': { name: 'videoStudyQuestionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionUpdate': { name: 'videoStudyQuestionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoSubtitleCreate': { name: 'videoSubtitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleDelete': { name: 'videoSubtitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleUpdate': { name: 'videoSubtitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoTitleCreate': { name: 'videoTitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleDelete': { name: 'videoTitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleUpdate': { name: 'videoTitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoUpdate': { name: 'videoUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoVariantCreate': { name: 'videoVariantCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDelete': { name: 'videoVariantDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDownloadCreate': { name: 'videoVariantDownloadCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadDelete': { name: 'videoVariantDownloadDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadUpdate': { name: 'videoVariantDownloadUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantUpdate': { name: 'videoVariantUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'visitorUpdate': { name: 'visitorUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; 'visitorUpdateForCurrentUser': { name: 'visitorUpdateForCurrentUser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; }; }; 'MutationAudioPreviewCreateInput': { kind: 'INPUT_OBJECT'; name: 'MutationAudioPreviewCreateInput'; isOneOf: false; inputFields: [{ name: 'languageId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'value'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'duration'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'size'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'bitrate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'codec'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'MutationAudioPreviewUpdateInput': { kind: 'INPUT_OBJECT'; name: 'MutationAudioPreviewUpdateInput'; isOneOf: false; inputFields: [{ name: 'languageId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'value'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'duration'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'size'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'bitrate'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'codec'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'MutationBibleCitationCreateInput': { kind: 'INPUT_OBJECT'; name: 'MutationBibleCitationCreateInput'; isOneOf: false; inputFields: [{ name: 'id'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'osisId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'videoId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'bibleBookId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'chapterStart'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'chapterEnd'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'verseStart'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'verseEnd'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'order'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }]; }; From bf441973eacc2471d24695b2fe836f5b18d7e0dc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:17:09 +0000 Subject: [PATCH 10/10] fix: lint issues (attempt 2/3) --- .../src/schema/journeyAiEdit/journeyAiEdit.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts index 82dd1ac5bb1..ede55993454 100644 --- a/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts +++ b/apis/api-journeys-modern/src/schema/journeyAiEdit/journeyAiEdit.ts @@ -148,12 +148,10 @@ builder.mutationField('journeyAiEdit', (t) => } // 4. Prune history to last 10 turns - const prunedHistory = (input.history ?? []) - .slice(-10) - .map((m) => ({ - role: m.role as 'user' | 'assistant', - content: m.content - })) + const prunedHistory = (input.history ?? []).slice(-10).map((m) => ({ + role: m.role as 'user' | 'assistant', + content: m.content + })) // 5. Harden user message const hardenedMessage = hardenPrompt(input.message)