Merge remote-tracking branch 'upstream/develop' into feat/cleanup/dmg

This commit is contained in:
M3gA-Mind
2026-03-19 11:47:55 +05:30
3 changed files with 167 additions and 109 deletions
+88 -1
View File
@@ -18,6 +18,7 @@ import {
type ModelInfo,
type Tool,
} from '../services/api/inferenceApi';
import { type TeamUsage, creditsApi } from '../services/api/creditsApi';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import type { NotionPageSummary, NotionSummary, NotionUserProfile } from '../store/notionSlice';
import {
@@ -141,6 +142,10 @@ const Conversations = () => {
const [isLoadingModels, setIsLoadingModels] = useState(false);
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
// Budget state
const [teamUsage, setTeamUsage] = useState<TeamUsage | null>(null);
const [isLoadingBudget, setIsLoadingBudget] = useState(false);
const isDragging = useRef(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const lastPanelWidthRef = useRef(panelWidth);
@@ -219,6 +224,18 @@ const Conversations = () => {
.finally(() => setIsLoadingModels(false));
}, []);
// Fetch inference budget on mount
useEffect(() => {
setIsLoadingBudget(true);
creditsApi
.getTeamUsage()
.then(data => setTeamUsage(data))
.catch(() => {
// Budget unavailable — silently ignore
})
.finally(() => setIsLoadingBudget(false));
}, []);
// Remove thread fetching - threads are now loaded from Redux persist
// Sync URL → Redux: when URL has a threadId param, select that thread
@@ -995,6 +1012,34 @@ const Conversations = () => {
{/* Message Input */}
<div className="flex-shrink-0 border-t border-white/10 px-4 py-3">
{/* Budget depleted banner — show top-up CTA */}
{teamUsage && teamUsage.remainingUsd <= 0 && (
<div className="mb-3 p-3 rounded-xl bg-coral-500/10 border border-coral-500/20 flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
<svg
className="w-4 h-4 text-coral-400 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<p className="text-xs text-coral-300 truncate">
Daily inference budget exhausted. Top up to continue.
</p>
</div>
<button
onClick={() => navigate('/settings/billing')}
className="flex-shrink-0 px-3 py-1.5 rounded-lg bg-coral-500 hover:bg-coral-400 text-white text-xs font-medium transition-colors">
Top Up
</button>
</div>
)}
{/* Show warning if another thread is active */}
{activeThreadId && activeThreadId !== selectedThreadId && (
<div className="mb-3 p-2 rounded-lg bg-amber-500/10 border border-amber-500/20">
@@ -1004,7 +1049,7 @@ const Conversations = () => {
</p>
</div>
)}
{/* Model selector */}
{/* Model selector + budget indicator */}
<div className="flex items-center gap-2 mb-2">
{isLoadingModels ? (
<span className="text-xs text-stone-600">Loading models</span>
@@ -1030,6 +1075,48 @@ const Conversations = () => {
</select>
</>
)}
<div className="flex-1" />
{/* Budget indicator — circular */}
{(isLoadingBudget || teamUsage) && (() => {
const size = 22;
const r = 9;
const circ = 2 * Math.PI * r;
const pct = teamUsage
? Math.min(1, teamUsage.remainingUsd / teamUsage.cycleBudgetUsd)
: 0;
const dash = pct * circ;
return (
<div className="flex items-center gap-1.5" title={teamUsage ? `$${teamUsage.remainingUsd.toFixed(2)} of $${teamUsage.cycleBudgetUsd.toFixed(2)} remaining` : 'Loading budget…'}>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90">
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="currentColor" strokeWidth="2.5" className="text-white/10" />
{teamUsage ? (
<circle
cx={size / 2} cy={size / 2} r={r}
fill="none" stroke="currentColor" strokeWidth="2.5"
strokeDasharray={`${dash} ${circ}`}
strokeLinecap="round"
className={pct < 0.2 ? 'text-amber-500' : 'text-primary-500'}
style={{ transition: 'stroke-dasharray 0.3s ease' }}
/>
) : (
<circle
cx={size / 2} cy={size / 2} r={r}
fill="none" stroke="currentColor" strokeWidth="2.5"
strokeDasharray={`${circ * 0.25} ${circ}`}
strokeLinecap="round"
className="text-stone-600 animate-spin origin-center"
style={{ transformOrigin: `${size / 2}px ${size / 2}px` }}
/>
)}
</svg>
{teamUsage && (
<span className="text-[10px] text-stone-500">
${teamUsage.remainingUsd.toFixed(2)}
</span>
)}
</div>
);
})()}
</div>
{sendError && (
<div className="flex items-center justify-between mb-2">
+6 -12
View File
@@ -8,7 +8,6 @@ import { setOnboardedForUser } from '../../store/authSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import FeaturesStep from './steps/FeaturesStep';
import GetStartedStep from './steps/GetStartedStep';
import InviteCodeStep from './steps/InviteCodeStep';
import PrivacyStep from './steps/PrivacyStep';
const Onboarding = () => {
@@ -16,14 +15,13 @@ const Onboarding = () => {
const dispatch = useAppDispatch();
const user = useAppSelector(state => state.user.user);
const [currentStep, setCurrentStep] = useState(0);
const totalSteps = 4;
const totalSteps = 3;
// Lottie animation files for each step
const stepAnimations = [
'/lottie/trophy.json', // Step 1 - Invite Code
'/lottie/wave.json', // Step 2 - Features
'/lottie/safe3.json', // Step 3 - Privacy
'/lottie/trophy.json', // Step 4 - Get Started
'/lottie/wave.json', // Step 1 - Features
'/lottie/safe3.json', // Step 2 - Privacy
'/lottie/trophy.json', // Step 3 - Get Started
];
const handleNext = () => {
@@ -54,15 +52,11 @@ const Onboarding = () => {
const renderStep = () => {
switch (currentStep) {
case 1:
return <InviteCodeStep onNext={handleNext} />;
case 2:
return <FeaturesStep onNext={handleNext} />;
case 3:
return <PrivacyStep onNext={handleNext} />;
case 4:
case 2:
return <GetStartedStep onComplete={handleComplete} />;
default:
return <InviteCodeStep onNext={handleNext} />;
return <FeaturesStep onNext={handleNext} />;
}
};