fix(ui): add provider indicator, retry button, and optimistic locking

- U4: add blue info box showing supported providers

- W1: add inline retry button in error alert

- W4: track lastModified, handle 409 conflict with auto-refresh
This commit is contained in:
kaitranntt
2026-01-08 16:56:05 -05:00
parent ba19e1fcda
commit 35f28a6e77
2 changed files with 54 additions and 6 deletions
@@ -1,9 +1,10 @@
/**
* Thinking Config Hook
* Manages thinking budget configuration with direct API calls
* Includes W4 optimistic locking via lastModified timestamps
*/
import { useCallback, useState } from 'react';
import { useCallback, useState, useRef } from 'react';
import type { ThinkingConfig, ThinkingMode } from '../types';
const DEFAULT_THINKING_CONFIG: ThinkingConfig = {
@@ -24,6 +25,8 @@ export function useThinkingConfig() {
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
// W4: Track lastModified for optimistic locking
const lastModifiedRef = useRef<number | undefined>(undefined);
const fetchConfig = useCallback(async () => {
const controller = new AbortController();
@@ -37,6 +40,8 @@ export function useThinkingConfig() {
if (!res.ok) throw new Error('Failed to load Thinking config');
const data = await res.json();
setConfig(data.config || DEFAULT_THINKING_CONFIG);
// W4: Store lastModified for optimistic locking
lastModifiedRef.current = data.lastModified;
} catch (err) {
clearTimeout(timeoutId);
if ((err as Error).name === 'AbortError') {
@@ -63,10 +68,16 @@ export function useThinkingConfig() {
setSaving(true);
setError(null);
// W4: Include lastModified for optimistic locking
const payload = {
...optimisticConfig,
lastModified: lastModifiedRef.current,
};
const res = await fetch('/api/thinking', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(optimisticConfig),
body: JSON.stringify(payload),
signal: controller.signal,
});
@@ -74,11 +85,17 @@ export function useThinkingConfig() {
if (!res.ok) {
const data = await res.json();
// W4: Handle conflict (409) with user-friendly message
if (res.status === 409) {
throw new Error('Config changed by another session. Refreshing...');
}
throw new Error(data.error || 'Failed to save');
}
const data = await res.json();
setConfig(data.config);
// W4: Update lastModified after successful save
lastModifiedRef.current = data.lastModified;
setSuccess(true);
setTimeout(() => setSuccess(false), 1500);
} catch (err) {
@@ -88,12 +105,16 @@ export function useThinkingConfig() {
setError('Request timeout - please try again');
} else {
setError((err as Error).message);
// W4: On conflict, auto-refresh to get latest
if ((err as Error).message.includes('another session')) {
setTimeout(() => fetchConfig(), 1000);
}
}
} finally {
setSaving(false);
}
},
[config]
[config, fetchConfig]
);
const setMode = useCallback(
@@ -16,7 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { RefreshCw, CheckCircle2, AlertCircle, Brain } from 'lucide-react';
import { RefreshCw, CheckCircle2, AlertCircle, Brain, Info } from 'lucide-react';
import { useThinkingConfig } from '../../hooks';
import type { ThinkingMode } from '../../types';
@@ -69,8 +69,22 @@ export default function ThinkingSection() {
>
{error && (
<Alert variant="destructive" className="py-2 shadow-lg">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</div>
{/* W1: Retry button on error */}
<Button
variant="outline"
size="sm"
onClick={fetchConfig}
className="h-7 px-2 text-xs border-destructive/50 hover:bg-destructive/10"
>
<RefreshCw className="w-3 h-3 mr-1" />
Retry
</Button>
</div>
</Alert>
)}
{success && (
@@ -91,6 +105,19 @@ export default function ThinkingSection() {
</p>
</div>
{/* U4: Provider support indicator */}
<div className="flex items-start gap-2 p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5" />
<div className="text-sm text-blue-700 dark:text-blue-300">
<p className="font-medium">Supported Providers</p>
<p className="mt-1 text-blue-600 dark:text-blue-400">
Thinking budget works with: <strong>agy</strong> (Antigravity),{' '}
<strong>gemini</strong> (with thinking models). Other providers may ignore this
setting.
</p>
</div>
</div>
{/* Mode Selection */}
<div className="space-y-3">
<h3 className="text-base font-medium">Thinking Mode</h3>