Auto-compact Threshold UI improvements (#5354)
This commit is contained in:
@@ -201,7 +201,7 @@ pub async fn check_if_compaction_needed(
|
|||||||
let usage_ratio = current_tokens as f64 / context_limit as f64;
|
let usage_ratio = current_tokens as f64 / context_limit as f64;
|
||||||
|
|
||||||
let needs_compaction = if threshold <= 0.0 || threshold >= 1.0 {
|
let needs_compaction = if threshold <= 0.0 || threshold >= 1.0 {
|
||||||
usage_ratio > DEFAULT_COMPACTION_THRESHOLD
|
false // Auto-compact is disabled.
|
||||||
} else {
|
} else {
|
||||||
usage_ratio > threshold
|
usage_ratio > threshold
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ export default function ChatInput({
|
|||||||
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
|
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
|
||||||
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
||||||
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
|
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
|
||||||
const [autoCompactThreshold, setAutoCompactThreshold] = useState<number>(0.8); // Default to 80%
|
|
||||||
|
|
||||||
// Draft functionality - get chat context and global draft context
|
// Draft functionality - get chat context and global draft context
|
||||||
// We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub)
|
// We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub)
|
||||||
@@ -501,22 +500,6 @@ export default function ChatInput({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentModel, currentProvider]);
|
}, [currentModel, currentProvider]);
|
||||||
|
|
||||||
// Load auto-compact threshold
|
|
||||||
const loadAutoCompactThreshold = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const threshold = await read('GOOSE_AUTO_COMPACT_THRESHOLD', false);
|
|
||||||
if (threshold !== undefined && threshold !== null) {
|
|
||||||
setAutoCompactThreshold(threshold as number);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching auto-compact threshold:', err);
|
|
||||||
}
|
|
||||||
}, [read]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadAutoCompactThreshold();
|
|
||||||
}, [loadAutoCompactThreshold]);
|
|
||||||
|
|
||||||
// Handle tool count alerts and token usage
|
// Handle tool count alerts and token usage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearAlerts();
|
clearAlerts();
|
||||||
@@ -542,10 +525,6 @@ export default function ChatInput({
|
|||||||
handleSubmit(customEvent);
|
handleSubmit(customEvent);
|
||||||
},
|
},
|
||||||
compactIcon: <ScrollText size={12} />,
|
compactIcon: <ScrollText size={12} />,
|
||||||
autoCompactThreshold: autoCompactThreshold,
|
|
||||||
onThresholdChange: (newThreshold: number) => {
|
|
||||||
setAutoCompactThreshold(newThreshold);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,15 +542,7 @@ export default function ChatInput({
|
|||||||
}
|
}
|
||||||
// We intentionally omit setView as it shouldn't trigger a re-render of alerts
|
// We intentionally omit setView as it shouldn't trigger a re-render of alerts
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [numTokens, toolCount, tokenLimit, isTokenLimitLoaded, addAlert, clearAlerts]);
|
||||||
numTokens,
|
|
||||||
toolCount,
|
|
||||||
tokenLimit,
|
|
||||||
isTokenLimitLoaded,
|
|
||||||
addAlert,
|
|
||||||
clearAlerts,
|
|
||||||
autoCompactThreshold,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Cleanup effect for component unmount - prevent memory leaks
|
// Cleanup effect for component unmount - prevent memory leaks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io';
|
import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io';
|
||||||
import { FaPencilAlt, FaSave } from 'react-icons/fa';
|
import { FaPencilAlt, FaSave } from 'react-icons/fa';
|
||||||
import { cn } from '../../utils';
|
import { cn } from '../../utils';
|
||||||
import { Alert, AlertType } from './types';
|
import { Alert, AlertType } from './types';
|
||||||
import { upsertConfig } from '../../api';
|
import { upsertConfig } from '../../api';
|
||||||
|
import { useConfig } from '../ConfigContext';
|
||||||
|
|
||||||
const alertIcons: Record<AlertType, React.ReactNode> = {
|
const alertIcons: Record<AlertType, React.ReactNode> = {
|
||||||
[AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />,
|
[AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />,
|
||||||
@@ -24,17 +25,36 @@ const alertStyles: Record<AlertType, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
||||||
|
const { read } = useConfig();
|
||||||
const [isEditingThreshold, setIsEditingThreshold] = useState(false);
|
const [isEditingThreshold, setIsEditingThreshold] = useState(false);
|
||||||
|
const [loadedThreshold, setLoadedThreshold] = useState<number | null>(null);
|
||||||
const [thresholdValue, setThresholdValue] = useState(
|
const [thresholdValue, setThresholdValue] = useState(
|
||||||
alert.autoCompactThreshold ? Math.round(alert.autoCompactThreshold * 100) : 80
|
alert.autoCompactThreshold ? Math.max(1, Math.round(alert.autoCompactThreshold * 100)) : 80
|
||||||
);
|
);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadThreshold = async () => {
|
||||||
|
try {
|
||||||
|
const threshold = await read('GOOSE_AUTO_COMPACT_THRESHOLD', false);
|
||||||
|
if (threshold !== undefined && threshold !== null) {
|
||||||
|
setLoadedThreshold(threshold as number);
|
||||||
|
setThresholdValue(Math.max(1, Math.round((threshold as number) * 100)));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching auto-compact threshold:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadThreshold();
|
||||||
|
}, [read]);
|
||||||
|
|
||||||
|
const currentThreshold = loadedThreshold !== null ? loadedThreshold : alert.autoCompactThreshold;
|
||||||
|
|
||||||
const handleSaveThreshold = async () => {
|
const handleSaveThreshold = async () => {
|
||||||
if (isSaving) return; // Prevent double-clicks
|
if (isSaving) return; // Prevent double-clicks
|
||||||
|
|
||||||
// Validate threshold value - allow 0 and 100 as special values to disable
|
let validThreshold = Math.max(1, Math.min(100, thresholdValue));
|
||||||
const validThreshold = Math.max(0, Math.min(100, thresholdValue));
|
|
||||||
if (validThreshold !== thresholdValue) {
|
if (validThreshold !== thresholdValue) {
|
||||||
setThresholdValue(validThreshold);
|
setThresholdValue(validThreshold);
|
||||||
}
|
}
|
||||||
@@ -52,6 +72,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIsEditingThreshold(false);
|
setIsEditingThreshold(false);
|
||||||
|
setLoadedThreshold(newThreshold);
|
||||||
|
|
||||||
// Notify parent component of the threshold change
|
// Notify parent component of the threshold change
|
||||||
if (alert.onThresholdChange) {
|
if (alert.onThresholdChange) {
|
||||||
@@ -82,14 +103,14 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
|||||||
<span className="text-[11px]">{alert.message}</span>
|
<span className="text-[11px]">{alert.message}</span>
|
||||||
|
|
||||||
{/* Auto-compact threshold indicator with edit */}
|
{/* Auto-compact threshold indicator with edit */}
|
||||||
{alert.autoCompactThreshold !== undefined && (
|
{currentThreshold !== undefined && (
|
||||||
<div className="flex items-center justify-center gap-1 min-h-[20px]">
|
<div className="flex items-center justify-center gap-1 min-h-[20px]">
|
||||||
{isEditingThreshold ? (
|
{isEditingThreshold ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-[10px] opacity-70">Auto summarize at</span>
|
<span className="text-[10px] opacity-70">Auto compact at</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
step="1"
|
step="1"
|
||||||
value={thresholdValue}
|
value={thresholdValue}
|
||||||
@@ -97,17 +118,17 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
|||||||
const val = parseInt(e.target.value, 10);
|
const val = parseInt(e.target.value, 10);
|
||||||
// Allow empty input for easier editing
|
// Allow empty input for easier editing
|
||||||
if (e.target.value === '') {
|
if (e.target.value === '') {
|
||||||
setThresholdValue(0);
|
setThresholdValue(1);
|
||||||
} else if (!isNaN(val)) {
|
} else if (!isNaN(val)) {
|
||||||
// Clamp value between 0 and 100
|
// Clamp value between 1 and 100
|
||||||
setThresholdValue(Math.max(0, Math.min(100, val)));
|
setThresholdValue(Math.max(1, Math.min(100, val)));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
// On blur, ensure we have a valid value
|
// On blur, ensure we have a valid value
|
||||||
const val = parseInt(e.target.value, 10);
|
const val = parseInt(e.target.value, 10);
|
||||||
if (isNaN(val) || val < 0) {
|
if (isNaN(val) || val < 1) {
|
||||||
setThresholdValue(0);
|
setThresholdValue(1);
|
||||||
} else if (val > 100) {
|
} else if (val > 100) {
|
||||||
setThresholdValue(100);
|
setThresholdValue(100);
|
||||||
}
|
}
|
||||||
@@ -117,11 +138,10 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
|||||||
handleSaveThreshold();
|
handleSaveThreshold();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
setIsEditingThreshold(false);
|
setIsEditingThreshold(false);
|
||||||
setThresholdValue(
|
const resetValue = currentThreshold
|
||||||
alert.autoCompactThreshold
|
? Math.round(currentThreshold * 100)
|
||||||
? Math.round(alert.autoCompactThreshold * 100)
|
: 80;
|
||||||
: 80
|
setThresholdValue(Math.max(1, resetValue));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
@@ -154,9 +174,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="text-[10px] opacity-70">
|
<span className="text-[10px] opacity-70">
|
||||||
{alert.autoCompactThreshold === 0 || alert.autoCompactThreshold === 1
|
Auto compact at {Math.round(currentThreshold * 100)}%
|
||||||
? 'Auto summarize disabled'
|
|
||||||
: `Auto summarize at ${Math.round(alert.autoCompactThreshold * 100)}%`}
|
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -176,46 +194,56 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between w-full relative">
|
<div className="flex justify-between w-full relative">
|
||||||
{[...Array(30)].map((_, i) => {
|
{(() => {
|
||||||
const progress = alert.progress!.current / alert.progress!.total;
|
let closestDotIndex = -1;
|
||||||
const progressPercentage = Math.round(progress * 100);
|
if (currentThreshold !== undefined && currentThreshold > 0 && currentThreshold <= 1) {
|
||||||
const dotPosition = i / 29; // 0 to 1 range for 30 dots
|
let minDistance = Infinity;
|
||||||
const isActive = dotPosition <= progress;
|
for (let j = 0; j < 30; j++) {
|
||||||
const isThresholdDot =
|
const dotPos = j / 29;
|
||||||
alert.autoCompactThreshold !== undefined &&
|
const distance = Math.abs(dotPos - currentThreshold);
|
||||||
alert.autoCompactThreshold > 0 &&
|
if (distance < minDistance) {
|
||||||
alert.autoCompactThreshold < 1 &&
|
minDistance = distance;
|
||||||
Math.abs(dotPosition - alert.autoCompactThreshold) < 0.017; // ~1/30 tolerance
|
closestDotIndex = j;
|
||||||
|
}
|
||||||
// Determine the color based on progress percentage
|
|
||||||
const getProgressColor = () => {
|
|
||||||
if (progressPercentage <= 50) {
|
|
||||||
return 'bg-green-500'; // Green for 0-50%
|
|
||||||
} else if (progressPercentage <= 75) {
|
|
||||||
return 'bg-yellow-500'; // Yellow for 51-75%
|
|
||||||
} else if (progressPercentage <= 90) {
|
|
||||||
return 'bg-orange-500'; // Orange for 76-90%
|
|
||||||
} else {
|
|
||||||
return 'bg-red-500'; // Red for 91-100%
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const progressColor = getProgressColor();
|
return [...Array(30)].map((_, i) => {
|
||||||
const inactiveColor = 'bg-gray-300 dark:bg-gray-600';
|
const progress = alert.progress!.current / alert.progress!.total;
|
||||||
|
const progressPercentage = Math.round(progress * 100);
|
||||||
|
const dotPosition = i / 29; // 0 to 1 range for 30 dots
|
||||||
|
const isActive = dotPosition <= progress;
|
||||||
|
const isThresholdDot = i === closestDotIndex;
|
||||||
|
|
||||||
return (
|
const getProgressColor = () => {
|
||||||
<div
|
if (progressPercentage <= 50) {
|
||||||
key={i}
|
return 'bg-green-500';
|
||||||
className={cn(
|
} else if (progressPercentage <= 75) {
|
||||||
'rounded-full transition-all relative',
|
return 'bg-yellow-500';
|
||||||
isThresholdDot
|
} else if (progressPercentage <= 90) {
|
||||||
? 'h-[6px] w-[6px] -mt-[2px]' // Make threshold dot twice as large
|
return 'bg-orange-500';
|
||||||
: 'h-[2px] w-[2px]',
|
} else {
|
||||||
isActive ? progressColor : inactiveColor
|
return 'bg-red-500';
|
||||||
)}
|
}
|
||||||
/>
|
};
|
||||||
);
|
|
||||||
})}
|
const progressColor = getProgressColor();
|
||||||
|
const inactiveColor = 'bg-gray-300 dark:bg-gray-600';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
'rounded-full transition-all relative',
|
||||||
|
isThresholdDot
|
||||||
|
? 'h-[6px] w-[6px] -mt-[2px]' // Make threshold dot twice as large
|
||||||
|
: 'h-[2px] w-[2px]',
|
||||||
|
isActive ? progressColor : inactiveColor
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-baseline text-[11px]">
|
<div className="flex justify-between items-baseline text-[11px]">
|
||||||
<div className="flex gap-1 items-baseline">
|
<div className="flex gap-1 items-baseline">
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { AlertBox } from '../AlertBox';
|
import { AlertBox } from '../AlertBox';
|
||||||
import { Alert, AlertType } from '../types';
|
import { Alert, AlertType } from '../types';
|
||||||
|
|
||||||
|
// Mock the ConfigContext
|
||||||
|
vi.mock('../../ConfigContext', () => ({
|
||||||
|
useConfig: () => ({
|
||||||
|
read: vi.fn().mockResolvedValue(0.8),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AlertBox', () => {
|
describe('AlertBox', () => {
|
||||||
const mockOnCompact = vi.fn();
|
const mockOnCompact = vi.fn();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user