Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions web/default/src/components/ai-elements/web-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import {
createContext,
type ReactNode,
useContext,
useEffect,
useState,
} from 'react'
import { ChevronDownIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils'
import { useResettableState } from '@/hooks/use-resettable-state'
import { Button } from '@/components/ui/button'
import {
Collapsible,
Expand Down Expand Up @@ -162,12 +162,7 @@ export const WebPreviewUrl = ({
}: WebPreviewUrlProps) => {
const { t } = useTranslation()
const { url, setUrl } = useWebPreview()
const [inputValue, setInputValue] = useState(url)

// Sync input value with context URL when it changes externally
useEffect(() => {
setInputValue(url)
}, [url])
const [inputValue, setInputValue] = useResettableState(url, url)

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value)
Expand Down
22 changes: 12 additions & 10 deletions web/default/src/components/risk-acknowledgement-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
*/
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { useResettableState } from '@/hooks/use-resettable-state'
import {
AlertDialog,
AlertDialogCancel,
Expand Down Expand Up @@ -83,9 +84,6 @@ export function RiskAcknowledgementDialog({
className,
}: RiskAcknowledgementDialogProps) {
const { t } = useTranslation()
const [checkedItems, setCheckedItems] = useState<boolean[]>([])
const [typedText, setTypedText] = useState('')
const [typedTextParts, setTypedTextParts] = useState<string[]>([])

const normalizedRequiredTextParts = useMemo<
NormalizedRequiredTextPart[]
Expand Down Expand Up @@ -114,12 +112,16 @@ export function RiskAcknowledgementDialog({
? normalizedRequiredTextParts.map((part) => part.text).join('')
: requiredText

useEffect(() => {
if (!open) return
setCheckedItems(Array(checklist.length).fill(false))
setTypedText('')
setTypedTextParts(Array(requiredTextInputCount).fill(''))
}, [open, checklist.length, requiredTextInputCount])
const draftKey = `${open}:${checklist.length}:${requiredTextInputCount}`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const [checkedItems, setCheckedItems] = useResettableState<boolean[]>(
() => Array(checklist.length).fill(false),
draftKey
)
const [typedText, setTypedText] = useResettableState('', draftKey)
const [typedTextParts, setTypedTextParts] = useResettableState<string[]>(
() => Array(requiredTextInputCount).fill(''),
draftKey
)

const allChecked = useMemo(() => {
if (checklist.length === 0) return true
Expand Down
17 changes: 9 additions & 8 deletions web/default/src/features/auth/hooks/use-oauth-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,21 @@ type LogoutRequestConfig = AxiosRequestConfig & {
export function useOAuthLogin(status: SystemStatus | null) {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(false)
const [githubButtonText, setGithubButtonText] = useState('')
const [githubButtonMessage, setGithubButtonText] = useState(
'Continue with GitHub'
)
const githubButtonText = t(githubButtonMessage)
const [githubButtonDisabled, setGithubButtonDisabled] = useState(false)
const githubTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const { auth } = useAuthStore()

useEffect(() => {
setGithubButtonText(t('Continue with GitHub'))

return () => {
if (githubTimeoutRef.current) {
clearTimeout(githubTimeoutRef.current)
}
}
}, [t])
}, [])

const resetSession = async () => {
try {
Expand All @@ -77,7 +78,7 @@ export function useOAuthLogin(status: SystemStatus | null) {

setIsLoading(true)
setGithubButtonDisabled(true)
setGithubButtonText(t('Redirecting to GitHub...'))
setGithubButtonText('Redirecting to GitHub...')

if (githubTimeoutRef.current) {
clearTimeout(githubTimeoutRef.current)
Expand All @@ -86,7 +87,7 @@ export function useOAuthLogin(status: SystemStatus | null) {
githubTimeoutRef.current = setTimeout(() => {
setIsLoading(false)
setGithubButtonText(
t('Request timed out, please refresh and restart GitHub login')
'Request timed out, please refresh and restart GitHub login'
)
setGithubButtonDisabled(true)
}, 20000)
Expand All @@ -100,7 +101,7 @@ export function useOAuthLogin(status: SystemStatus | null) {
clearTimeout(githubTimeoutRef.current)
}
setIsLoading(false)
setGithubButtonText(t('Continue with GitHub'))
setGithubButtonText('Continue with GitHub')
setGithubButtonDisabled(false)
return
}
Expand All @@ -113,7 +114,7 @@ export function useOAuthLogin(status: SystemStatus | null) {
clearTimeout(githubTimeoutRef.current)
}
setIsLoading(false)
setGithubButtonText(t('Continue with GitHub'))
setGithubButtonText('Continue with GitHub')
setGithubButtonDisabled(false)
}
}
Expand Down
33 changes: 31 additions & 2 deletions web/default/src/features/auth/lib/app-session-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,41 @@ For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API
*/
import { useAuthStore } from '@/stores/auth-store'
import { getSelf } from '@/lib/api'
import { isStaleAuthResponse } from '@/lib/auth-errors'
import { createSessionVerifier } from './session-verifier'

async function getSelfForVerification() {
while (true) {
const starting = useAuthStore.getState()
try {
const response = await getSelf()
const current = useAuthStore.getState()
// Authenticated identities include the revision below. When both are
// anonymous, the verifier cannot see a reset, so refresh this pure read.
if (
current.sessionRevision === starting.sessionRevision ||
starting.auth.user ||
current.auth.user
)
return response
} catch (error) {
const current = useAuthStore.getState()
if (
isStaleAuthResponse(error) &&
!starting.auth.user &&
!current.auth.user
)
continue
throw error
}
}
}

export const appSessionVerifier = createSessionVerifier({
getAuthState: () => useAuthStore.getState().auth,
getSelf,
getUserIdentity: (user) => user.id,
getSelf: getSelfForVerification,
getUserIdentity: (user) =>
`${useAuthStore.getState().sessionRevision}:${user.id}`,
isUnauthorized: (error: unknown) =>
(error as { response?: { status?: number } })?.response?.status === 401,
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
*/
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useEffectEvent } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { Loader2, RefreshCw, DollarSign } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { formatCurrencyFromUSD } from '@/lib/currency'
import { formatTimestampToDate } from '@/lib/format'
import { useReadScope } from '@/hooks/use-read-scope'
import { useResettableState } from '@/hooks/use-resettable-state'
import { Button } from '@/components/ui/button'
import {
Dialog,
Expand Down Expand Up @@ -52,48 +54,67 @@ export function BalanceQueryDialog({
const { t } = useTranslation()
const { currentRow, setCurrentRow } = useChannels()
const queryClient = useQueryClient()
const [isQuerying, setIsQuerying] = useState(false)
const [balance, setBalance] = useState<number | null>(null)
const [balanceUpdatedTime, setBalanceUpdatedTime] = useState<number | null>(
null
const channelId = currentRow?.id
const isCodex = currentRow?.type === 57
const beginRead = useReadScope(JSON.stringify([open, channelId]))
const [isQuerying, setIsQuerying] = useResettableState(
Boolean(open && isCodex && channelId),
beginRead
)
const [balance, setBalance] = useResettableState<number | null>(
null,
beginRead
)
const [balanceUpdatedTime, setBalanceUpdatedTime] = useResettableState<
number | null
>(null, beginRead)
const [codexUsageResponse, setCodexUsageResponse] =
useState<CodexUsageDialogData | null>(null)

const isCodex = currentRow?.type === 57

const handleQueryCodexUsage = async () => {
const row = currentRow
if (!row) return
useResettableState<CodexUsageDialogData | null>(null, beginRead)
const loadCodexUsage = useCallback(() => {
if (!channelId) return Promise.resolve()
const current = beginRead()
if (!current()) return Promise.resolve()
return getCodexUsage(channelId)
.then((res) => {
if (!current()) return
if (!res.success)
throw new Error(res.message || t('Failed to fetch usage'))
setCodexUsageResponse(res)
})
.catch((error: unknown) => {
if (current())
toast.error(
error instanceof Error ? error.message : t('Failed to fetch usage')
)
})
.finally(() => {
if (current()) setIsQuerying(false)
})
}, [channelId, beginRead, setCodexUsageResponse, setIsQuerying, t])
const handleQueryCodexUsage = () => {
setIsQuerying(true)
try {
const res = await getCodexUsage(row.id)
if (!res.success) {
throw new Error(res.message || t('Failed to fetch usage'))
}
setCodexUsageResponse(res)
} catch (error: unknown) {
toast.error(
error instanceof Error ? error.message : t('Failed to fetch usage')
)
} finally {
setIsQuerying(false)
}
return loadCodexUsage()
}

const readInitialUsage = useEffectEvent(() => loadCodexUsage())
useEffect(() => {
if (!isCodex) return
if (!open) return
handleQueryCodexUsage()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, isCodex])
if (open && isCodex) void readInitialUsage()
}, [open, isCodex, channelId, beginRead])

if (!currentRow) return null

const handleQueryBalance = async () => {
const current = beginRead()
if (!current()) return
setIsQuerying(true)
try {
const response = await updateChannelBalance(currentRow.id)
if (!current()) {
if (response.success && response.balance !== undefined)
void queryClient.invalidateQueries({
queryKey: channelsQueryKeys.lists(),
})
return
}
if (response.success && response.balance !== undefined) {
const newBalance = response.balance
const now = Math.floor(Date.now() / 1000)
Expand All @@ -117,11 +138,12 @@ export function BalanceQueryDialog({
toast.error(response.message || t('Failed to query balance'))
}
} catch (error: unknown) {
if (!current()) return
toast.error(
error instanceof Error ? error.message : t('Failed to query balance')
)
} finally {
setIsQuerying(false)
if (current()) setIsQuerying(false)
}
}

Expand Down
Loading
Loading