feat: add DeepChat deeplink support (#4668)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import { useMemo, useCallback, useRef, useState } from 'react'
|
||||
import { Link, useLocation } from '@tanstack/react-router'
|
||||
import { ExternalLink, Loader2, ChevronRight } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
@@ -23,31 +22,30 @@ import {
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { useActiveChatKey } from '@/features/chat/hooks/use-active-chat-key'
|
||||
import { fetchActiveChatKey } from '@/features/chat/hooks/use-active-chat-key'
|
||||
import { useChatPresets } from '@/features/chat/hooks/use-chat-presets'
|
||||
import { resolveChatUrl, type ChatPreset } from '@/features/chat/lib/chat-links'
|
||||
import {
|
||||
chatLinkRequiresApiKey,
|
||||
resolveChatUrl,
|
||||
type ChatPreset,
|
||||
} from '@/features/chat/lib/chat-links'
|
||||
import { normalizeHref } from '../lib/url-utils'
|
||||
import type { NavChatPresets } from '../types'
|
||||
|
||||
/**
|
||||
* Check if a preset requires an API key
|
||||
*/
|
||||
function requiresApiKey(preset: ChatPreset): boolean {
|
||||
return preset.url.includes('{key}') || preset.url.includes('{cherryConfig}')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-menu item for a single chat preset
|
||||
*/
|
||||
function ChatMenuItem({
|
||||
preset,
|
||||
active,
|
||||
loading,
|
||||
onOpen,
|
||||
onNavigate,
|
||||
}: {
|
||||
preset: ChatPreset
|
||||
active: boolean
|
||||
onOpen: (preset: ChatPreset) => void
|
||||
loading: boolean
|
||||
onOpen: (preset: ChatPreset) => void | Promise<void>
|
||||
onNavigate: () => void
|
||||
}) {
|
||||
if (preset.type === 'web') {
|
||||
@@ -72,12 +70,19 @@ function ChatMenuItem({
|
||||
return (
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
onClick={() => onOpen(preset)}
|
||||
onClick={() => {
|
||||
if (!loading) void onOpen(preset)
|
||||
}}
|
||||
aria-disabled={loading ? 'true' : undefined}
|
||||
isActive={false}
|
||||
className='justify-between'
|
||||
>
|
||||
<span>{preset.name}</span>
|
||||
<ExternalLink className='h-4 w-4' />
|
||||
{loading ? (
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<ExternalLink className='h-4 w-4' />
|
||||
)}
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
)
|
||||
@@ -88,10 +93,12 @@ function ChatMenuItem({
|
||||
*/
|
||||
function DropdownPresetItem({
|
||||
preset,
|
||||
loading,
|
||||
onOpen,
|
||||
}: {
|
||||
preset: ChatPreset
|
||||
onOpen: (preset: ChatPreset) => void
|
||||
loading: boolean
|
||||
onOpen: (preset: ChatPreset) => void | Promise<void>
|
||||
}) {
|
||||
if (preset.type === 'web') {
|
||||
return (
|
||||
@@ -104,9 +111,18 @@ function DropdownPresetItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem onClick={() => onOpen(preset)}>
|
||||
<DropdownMenuItem
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
if (!loading) void onOpen(preset)
|
||||
}}
|
||||
>
|
||||
{preset.name}
|
||||
<ExternalLink className='ml-auto h-4 w-4 opacity-70' />
|
||||
{loading ? (
|
||||
<Loader2 className='ml-auto h-4 w-4 animate-spin opacity-70' />
|
||||
) : (
|
||||
<ExternalLink className='ml-auto h-4 w-4 opacity-70' />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
@@ -119,44 +135,44 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
||||
const { chatPresets, serverAddress } = useChatPresets()
|
||||
const { state, isMobile, setOpenMobile } = useSidebar()
|
||||
const href = useLocation({ select: (location) => location.href })
|
||||
const loadingMessage = t('Preparing chat keys…')
|
||||
const [loadingPresetId, setLoadingPresetId] = useState<string | null>(null)
|
||||
const loadingPresetIdRef = useRef<string | null>(null)
|
||||
|
||||
const visiblePresets = useMemo(
|
||||
() => chatPresets.filter((preset) => preset.type !== 'fluent'),
|
||||
[chatPresets]
|
||||
)
|
||||
|
||||
const hasKeyDependentPresets = useMemo(
|
||||
() => visiblePresets.some(requiresApiKey),
|
||||
[visiblePresets]
|
||||
)
|
||||
|
||||
const {
|
||||
data: activeKey,
|
||||
isPending: isKeyPending,
|
||||
error: keyError,
|
||||
} = useActiveChatKey(hasKeyDependentPresets)
|
||||
|
||||
const handleOpenExternal = useCallback(
|
||||
(preset: ChatPreset) => {
|
||||
async (preset: ChatPreset) => {
|
||||
if (preset.type === 'web') return
|
||||
|
||||
const needsKey = requiresApiKey(preset)
|
||||
const needsKey = chatLinkRequiresApiKey(preset.url)
|
||||
let activeKey: string | undefined
|
||||
|
||||
if (needsKey && isKeyPending) {
|
||||
if (needsKey && loadingPresetIdRef.current) {
|
||||
toast.info(t('Preparing your chat link, please try again in a moment.'))
|
||||
return
|
||||
}
|
||||
|
||||
if (needsKey && !activeKey) {
|
||||
const message =
|
||||
keyError instanceof Error
|
||||
? keyError.message
|
||||
: t(
|
||||
'Unable to prepare chat link. Please ensure you have an enabled API key.'
|
||||
)
|
||||
toast.error(message)
|
||||
return
|
||||
if (needsKey) {
|
||||
loadingPresetIdRef.current = preset.id
|
||||
setLoadingPresetId(preset.id)
|
||||
try {
|
||||
activeKey = await fetchActiveChatKey()
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t(
|
||||
'Unable to prepare chat link. Please ensure you have an enabled API key.'
|
||||
)
|
||||
toast.error(message)
|
||||
return
|
||||
} finally {
|
||||
loadingPresetIdRef.current = null
|
||||
setLoadingPresetId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const url = resolveChatUrl({
|
||||
@@ -175,7 +191,7 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
||||
window.open(url, '_blank', 'noopener')
|
||||
setOpenMobile(false)
|
||||
},
|
||||
[activeKey, isKeyPending, keyError, serverAddress, setOpenMobile, t]
|
||||
[serverAddress, setOpenMobile, t]
|
||||
)
|
||||
|
||||
const normalizedHref = normalizeHref(href)
|
||||
@@ -202,16 +218,10 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
||||
<DropdownPresetItem
|
||||
key={preset.id}
|
||||
preset={preset}
|
||||
loading={loadingPresetId === preset.id}
|
||||
onOpen={handleOpenExternal}
|
||||
/>
|
||||
))}
|
||||
{hasKeyDependentPresets && <DropdownMenuSeparator />}
|
||||
{hasKeyDependentPresets && isKeyPending && (
|
||||
<DropdownMenuItem disabled>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
{loadingMessage}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
@@ -240,18 +250,11 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
||||
key={preset.id}
|
||||
preset={preset}
|
||||
active={normalizedHref === `/chat/${preset.id}`}
|
||||
loading={loadingPresetId === preset.id}
|
||||
onOpen={handleOpenExternal}
|
||||
onNavigate={() => setOpenMobile(false)}
|
||||
/>
|
||||
))}
|
||||
{hasKeyDependentPresets && isKeyPending && (
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton aria-disabled='true' tabIndex={-1}>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
{loadingMessage}
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
)}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
Reference in New Issue
Block a user