mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user