feat: stream challenge submissions and tidy challenge imports

This commit is contained in:
Joey Yakimowich-Payne 2025-10-01 14:33:44 -06:00
commit cb4cde0ed2
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
13 changed files with 309 additions and 93 deletions

View file

@ -1,7 +1,32 @@
import { getPublic, postPublic } from './base'
import { getPublic, ssePost } from './base'
import { PUBLIC_API_PREFIX } from '@/config'
import { getInitialTokenV2, isTokenV1 } from '@/app/components/share/utils'
import { CONVERSATION_ID_INFO } from '@/app/components/base/chat/constants'
import type { WorkflowFinishedResponse } from '@/types/workflow'
type ChatMessageEnd = {
metadata?: {
outputs?: Record<string, any>
answer?: string
message?: string
judge_feedback?: string
judge_rating?: number
}
}
export type ChallengeAttemptResult = {
success: boolean
message: string
rating?: number
outputs: Record<string, any>
rawText: string
}
export type ChallengeAttemptCallbacks = {
onStreamUpdate?: (text: string) => void
onError?: (message: string) => void
onAbortController?: (abortController: AbortController | null) => void
}
export type ChallengeListItem = {
id: string
@ -31,11 +56,12 @@ export async function fetchChallengeLeaderboard(id: string) {
export async function submitChallengeAttempt(
challengeId: string,
appId: string,
_appId: string,
appSiteCode: string | undefined,
appMode: string,
userInput: string,
) {
callbacks?: ChallengeAttemptCallbacks,
): Promise<ChallengeAttemptResult> {
if (!appSiteCode)
throw new Error('Challenge app is not published. Please enable the app site for this challenge.')
@ -82,23 +108,147 @@ export async function submitChallengeAttempt(
localStorage.setItem(storageKey, JSON.stringify(tokenStore))
localStorage.removeItem(CONVERSATION_ID_INFO)
if (appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat') {
return await postPublic<any>('/chat-messages', {
body: {
query: userInput,
inputs: {},
response_mode: 'blocking',
conversation_id: '',
},
})
}
const isChatApp = appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat'
return await postPublic<any>('/workflows/run', {
body: {
inputs: {
user_prompt: userInput,
return await new Promise<ChallengeAttemptResult>((resolve, reject) => {
let aggregatedText = ''
let finalOutputs: Record<string, any> | undefined
let finalMessage: string | undefined
let isSettled = false
const releaseAbortController = () => {
callbacks?.onAbortController?.(null)
}
const emitStreamUpdate = () => {
callbacks?.onStreamUpdate?.(aggregatedText)
}
const settleError = (message: string) => {
if (isSettled)
return
isSettled = true
releaseAbortController()
const normalizedMessage = message || 'Submission failed'
const error = new Error(normalizedMessage)
if (callbacks?.onError)
(error as any).__handled = true
callbacks?.onError?.(normalizedMessage)
reject(error)
}
const buildResult = (): ChallengeAttemptResult => {
const outputs = finalOutputs || {}
const successFlag = Boolean(outputs.challenge_succeeded)
const rating = outputs.judge_rating ?? outputs.rating
const feedback = outputs.judge_feedback || outputs.message || finalMessage || aggregatedText
const message = feedback || (successFlag ? 'Challenge passed!' : 'Challenge not passed.')
return {
success: successFlag,
rating,
message,
outputs,
rawText: aggregatedText,
}
}
const settleSuccess = () => {
if (isSettled)
return
isSettled = true
releaseAbortController()
resolve(buildResult())
}
const commonOptions = {
isPublicAPI: true,
getAbortController: (abortController: AbortController) => {
callbacks?.onAbortController?.(abortController)
},
response_mode: 'blocking',
},
onError: (error: string) => {
const errorMessage = typeof error === 'string' ? error : 'Submission failed'
settleError(errorMessage)
},
onCompleted: (hasError?: boolean, errorMessage?: string) => {
if (hasError) {
settleError(errorMessage || 'Submission failed')
return
}
settleSuccess()
},
}
if (isChatApp) {
ssePost(
'/chat-messages',
{
body: {
query: userInput,
inputs: {},
response_mode: 'streaming',
conversation_id: '',
},
},
{
...commonOptions,
onData: (message: string) => {
aggregatedText += message
emitStreamUpdate()
},
onMessageReplace: (messageReplace) => {
aggregatedText = messageReplace.answer
emitStreamUpdate()
},
onMessageEnd: (messageEnd) => {
const metadata = (messageEnd as ChatMessageEnd).metadata
if (metadata?.outputs)
finalOutputs = metadata.outputs
const endMessage = metadata?.answer || metadata?.message || metadata?.judge_feedback
if (endMessage) {
aggregatedText = endMessage
emitStreamUpdate()
}
if (metadata?.judge_feedback)
finalMessage = metadata.judge_feedback
else if (metadata?.answer || metadata?.message)
finalMessage = metadata.answer || metadata.message
},
},
)
return
}
ssePost(
'/workflows/run',
{
body: {
inputs: {
user_prompt: userInput,
},
response_mode: 'streaming',
},
},
{
...commonOptions,
onTextChunk: (chunk) => {
const text = (chunk as any)?.data?.text || ''
if (text) {
aggregatedText += text
emitStreamUpdate()
}
},
onWorkflowFinished: ({ data }) => {
const resultData = (data as WorkflowFinishedResponse['data']) || {}
if (resultData.outputs)
finalOutputs = resultData.outputs
const message = resultData.outputs?.judge_feedback || resultData.outputs?.message
if (message) {
aggregatedText = message
emitStreamUpdate()
}
finalMessage = message || finalMessage
},
},
)
})
}