import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Code2, Copy, Eye, Plus, Trash2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' import { useUpdateOption } from '../hooks/use-update-option' const OPTION_KEY = 'tool_price_setting.prices' const DEFAULT_PRICES: Record = { web_search: 10.0, web_search_preview: 10.0, 'web_search_preview:gpt-4o*': 25.0, 'web_search_preview:gpt-4.1*': 25.0, 'web_search_preview:gpt-4o-mini*': 25.0, 'web_search_preview:gpt-4.1-mini*': 25.0, file_search: 2.5, google_search: 14.0, } type ToolPriceRow = { id: number key: string price: number } function rowsToObject(rows: ToolPriceRow[]): Record { const prices: Record = {} for (const row of rows) { const k = row.key.trim() if (!k) continue prices[k] = Number(row.price) || 0 } return prices } function objectToRows(prices: Record): ToolPriceRow[] { return Object.entries(prices).map(([key, price], index) => ({ id: index + 1, key, price: Number(price) || 0, })) } function parseInitialPrices( rawValue: string | undefined ): Record { if (!rawValue) return { ...DEFAULT_PRICES } try { const parsed = JSON.parse(rawValue) as unknown if ( parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed as object).length > 0 ) { return parsed as Record } } catch { // fall through to defaults } return { ...DEFAULT_PRICES } } type ToolPriceSettingsProps = { defaultValue: string } export const ToolPriceSettings = memo(function ToolPriceSettings({ defaultValue, }: ToolPriceSettingsProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const [editMode, setEditMode] = useState<'visual' | 'json'>('visual') const [rows, setRows] = useState([]) const [jsonText, setJsonText] = useState('') const [jsonError, setJsonError] = useState('') const [nextRowId, setNextRowId] = useState(1) useEffect(() => { const prices = parseInitialPrices(defaultValue) const initialRows = objectToRows(prices) // eslint-disable-next-line react-hooks/set-state-in-effect setRows(initialRows) setJsonText(JSON.stringify(prices, null, 2)) setJsonError('') setNextRowId(initialRows.length + 1) }, [defaultValue]) const currentPrices = useMemo(() => rowsToObject(rows), [rows]) const syncFromRows = useCallback((nextRows: ToolPriceRow[]) => { setRows(nextRows) setJsonText(JSON.stringify(rowsToObject(nextRows), null, 2)) setJsonError('') }, []) const handleJsonChange = useCallback( (text: string) => { setJsonText(text) try { const parsed = JSON.parse(text) as unknown if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { setJsonError(t('JSON must be an object')) return } const nextRows = objectToRows(parsed as Record) setRows(nextRows) setNextRowId(nextRows.length + 1) setJsonError('') } catch (error) { setJsonError(error instanceof Error ? error.message : t('Invalid JSON')) } }, [t] ) const updateRow = useCallback( (id: number, field: 'key' | 'price', value: string | number) => { syncFromRows( rows.map((r) => (r.id === id ? { ...r, [field]: value } : r)) ) }, [rows, syncFromRows] ) const addRow = useCallback(() => { const newRow: ToolPriceRow = { id: nextRowId, key: '', price: 0 } setNextRowId((prev) => prev + 1) syncFromRows([...rows, newRow]) }, [nextRowId, rows, syncFromRows]) const removeRow = useCallback( (id: number) => { syncFromRows(rows.filter((r) => r.id !== id)) }, [rows, syncFromRows] ) const resetToDefault = useCallback(() => { const initialRows = objectToRows(DEFAULT_PRICES) setRows(initialRows) setJsonText(JSON.stringify(DEFAULT_PRICES, null, 2)) setJsonError('') setNextRowId(initialRows.length + 1) }, []) const handleCopyJson = useCallback(async () => { try { await navigator.clipboard.writeText(jsonText) toast.success(t('Copied to clipboard')) } catch { toast.error(t('Failed to copy')) } }, [jsonText, t]) const handleSave = useCallback(async () => { if (editMode === 'json' && jsonError) { toast.error(t('Please fix JSON errors before saving')) return } await updateOption.mutateAsync({ key: OPTION_KEY, value: JSON.stringify(currentPrices), }) }, [currentPrices, editMode, jsonError, t, updateOption]) const toggleEditMode = useCallback(() => { setEditMode((prev) => (prev === 'visual' ? 'json' : 'visual')) }, []) return (
{t( 'Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.' )}
{t('Format')}:{' '} web_search_preview {' '} {t('is the default price; ')} web_search_preview:gpt-4o* {' '} {t('overrides for matching model prefix.')}
{editMode === 'visual' ? ( <> ) : ( <> )}
{editMode === 'visual' ? (
{t('Tool identifier')} {t('Price ($/1K calls)')} {t('Actions')} {rows.length === 0 ? ( {t('No tools configured')} ) : ( rows.map((row) => ( updateRow(row.id, 'key', e.target.value) } /> updateRow( row.id, 'price', Number(e.target.value) || 0 ) } /> )) )}
) : (