feat(desktop): add Configure button in chat error banner
When the agent fails due to missing API key, the error banner now shows a "Configure" button that opens the same ApiKeyDialog (or OAuthDialog) used on the home page. After successful configuration the error clears and the user can immediately start chatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8d32a06b5c
commit
ed681a96bf
3 changed files with 109 additions and 23 deletions
|
|
@ -1,6 +1,10 @@
|
|||
import { useState, useCallback } from 'react'
|
||||
import { Loading } from '@multica/ui/components/ui/loading'
|
||||
import { ChatView } from '@multica/ui/components/chat-view'
|
||||
import { useLocalChat } from '../hooks/use-local-chat'
|
||||
import { useProvider } from '../hooks/use-provider'
|
||||
import { ApiKeyDialog } from './api-key-dialog'
|
||||
import { OAuthDialog } from './oauth-dialog'
|
||||
|
||||
export function LocalChat() {
|
||||
const {
|
||||
|
|
@ -17,8 +21,41 @@ export function LocalChat() {
|
|||
sendMessage,
|
||||
loadMore,
|
||||
resolveApproval,
|
||||
clearError,
|
||||
} = useLocalChat()
|
||||
|
||||
const { providers, current, setProvider: switchProvider, refresh: refreshProviders } = useProvider()
|
||||
|
||||
// Provider config dialog state
|
||||
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false)
|
||||
const [oauthDialogOpen, setOauthDialogOpen] = useState(false)
|
||||
|
||||
const handleConfigureProvider = useCallback(() => {
|
||||
const providerId = current?.provider
|
||||
if (!providerId) return
|
||||
|
||||
const meta = providers.find((p) => p.id === providerId)
|
||||
if (!meta) return
|
||||
|
||||
if (meta.authMethod === 'oauth') {
|
||||
setOauthDialogOpen(true)
|
||||
} else {
|
||||
setApiKeyDialogOpen(true)
|
||||
}
|
||||
}, [current, providers])
|
||||
|
||||
const handleProviderConfigSuccess = useCallback(async () => {
|
||||
const providerId = current?.provider
|
||||
if (!providerId) return
|
||||
|
||||
await refreshProviders()
|
||||
await switchProvider(providerId)
|
||||
clearError()
|
||||
}, [current, refreshProviders, switchProvider, clearError])
|
||||
|
||||
// Derive provider info for dialogs
|
||||
const currentMeta = current ? providers.find((p) => p.id === current.provider) : null
|
||||
|
||||
if (initError) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-destructive">
|
||||
|
|
@ -36,19 +73,48 @@ export function LocalChat() {
|
|||
)
|
||||
}
|
||||
|
||||
// Show "Configure" button when error is about provider/API key
|
||||
const errorAction = error?.code === 'AGENT_ERROR' && currentMeta
|
||||
? { label: 'Configure', onClick: handleConfigureProvider }
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<ChatView
|
||||
messages={messages}
|
||||
streamingIds={streamingIds}
|
||||
isLoading={isLoading}
|
||||
isLoadingHistory={isLoadingHistory}
|
||||
isLoadingMore={isLoadingMore}
|
||||
hasMore={hasMore}
|
||||
error={error}
|
||||
pendingApprovals={pendingApprovals}
|
||||
sendMessage={sendMessage}
|
||||
loadMore={loadMore}
|
||||
resolveApproval={resolveApproval}
|
||||
/>
|
||||
<>
|
||||
<ChatView
|
||||
messages={messages}
|
||||
streamingIds={streamingIds}
|
||||
isLoading={isLoading}
|
||||
isLoadingHistory={isLoadingHistory}
|
||||
isLoadingMore={isLoadingMore}
|
||||
hasMore={hasMore}
|
||||
error={error}
|
||||
pendingApprovals={pendingApprovals}
|
||||
sendMessage={sendMessage}
|
||||
loadMore={loadMore}
|
||||
resolveApproval={resolveApproval}
|
||||
errorAction={errorAction}
|
||||
/>
|
||||
|
||||
{currentMeta && currentMeta.authMethod === 'api-key' && (
|
||||
<ApiKeyDialog
|
||||
open={apiKeyDialogOpen}
|
||||
onOpenChange={setApiKeyDialogOpen}
|
||||
providerId={currentMeta.id}
|
||||
providerName={currentMeta.name}
|
||||
onSuccess={handleProviderConfigSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentMeta && currentMeta.authMethod === 'oauth' && (
|
||||
<OAuthDialog
|
||||
open={oauthDialogOpen}
|
||||
onOpenChange={setOauthDialogOpen}
|
||||
providerId={currentMeta.id}
|
||||
providerName={currentMeta.name}
|
||||
loginCommand={currentMeta.loginCommand}
|
||||
onSuccess={handleProviderConfigSuccess}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,10 @@ export function useLocalChat() {
|
|||
[],
|
||||
)
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
chatRef.current.setError(null)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
agentId,
|
||||
initError,
|
||||
|
|
@ -156,5 +160,6 @@ export function useLocalChat() {
|
|||
sendMessage,
|
||||
loadMore,
|
||||
resolveApproval,
|
||||
clearError,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ export interface ChatViewProps {
|
|||
loadMore?: () => void;
|
||||
resolveApproval: (approvalId: string, decision: "allow-once" | "allow-always" | "deny") => void;
|
||||
onDisconnect?: () => void;
|
||||
/** Optional action button in the error banner (e.g. "Configure Provider") */
|
||||
errorAction?: { label: string; onClick: () => void };
|
||||
}
|
||||
|
||||
export function ChatView({
|
||||
|
|
@ -53,6 +55,7 @@ export function ChatView({
|
|||
loadMore,
|
||||
resolveApproval,
|
||||
onDisconnect,
|
||||
errorAction,
|
||||
}: ChatViewProps) {
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -219,16 +222,28 @@ export function ChatView({
|
|||
<div className="container px-4" role="alert" aria-live="polite">
|
||||
<div className="rounded-lg bg-destructive/5 border border-destructive/15 text-xs px-3 py-2 flex items-center justify-between gap-3">
|
||||
<span className="text-foreground leading-snug">{error.message}</span>
|
||||
{onDisconnect && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
className="shrink-0 text-xs h-7 px-2.5"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{errorAction && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={errorAction.onClick}
|
||||
className="shrink-0 text-xs h-7 px-2.5"
|
||||
>
|
||||
{errorAction.label}
|
||||
</Button>
|
||||
)}
|
||||
{onDisconnect && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
className="shrink-0 text-xs h-7 px-2.5"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue