mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 21:02:40 +02:00
feat(ui): surface pinned premium quota alerts in chat thread
This commit is contained in:
parent
835bd9f65d
commit
d5ef0d2598
3 changed files with 148 additions and 10 deletions
|
|
@ -19,6 +19,7 @@ import {
|
||||||
currentThreadAtom,
|
currentThreadAtom,
|
||||||
setTargetCommentIdAtom,
|
setTargetCommentIdAtom,
|
||||||
} from "@/atoms/chat/current-thread.atom";
|
} from "@/atoms/chat/current-thread.atom";
|
||||||
|
import { setPremiumAlertForThreadAtom } from "@/atoms/chat/premium-alert.atom";
|
||||||
import {
|
import {
|
||||||
type MentionedDocumentInfo,
|
type MentionedDocumentInfo,
|
||||||
mentionedDocumentIdsAtom,
|
mentionedDocumentIdsAtom,
|
||||||
|
|
@ -200,6 +201,19 @@ const BASE_TOOLS_WITH_UI = new Set([
|
||||||
// "write_todos", // Disabled for now
|
// "write_todos", // Disabled for now
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const PINNED_PREMIUM_QUOTA_MESSAGE = "Premium token quota exceeded for this pinned model.";
|
||||||
|
|
||||||
|
function getPinnedPremiumQuotaErrorMessage(error: unknown): string | null {
|
||||||
|
if (!(error instanceof Error)) return null;
|
||||||
|
if (!error.message.toLowerCase().includes("premium token quota exceeded")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!error.message.toLowerCase().includes("pinned model")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return error.message || PINNED_PREMIUM_QUOTA_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
export default function NewChatPage() {
|
export default function NewChatPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -226,6 +240,7 @@ export default function NewChatPage() {
|
||||||
const setMentionedDocuments = useSetAtom(mentionedDocumentsAtom);
|
const setMentionedDocuments = useSetAtom(mentionedDocumentsAtom);
|
||||||
const setMessageDocumentsMap = useSetAtom(messageDocumentsMapAtom);
|
const setMessageDocumentsMap = useSetAtom(messageDocumentsMapAtom);
|
||||||
const setCurrentThreadState = useSetAtom(currentThreadAtom);
|
const setCurrentThreadState = useSetAtom(currentThreadAtom);
|
||||||
|
const setPremiumAlertForThread = useSetAtom(setPremiumAlertForThreadAtom);
|
||||||
const setTargetCommentId = useSetAtom(setTargetCommentIdAtom);
|
const setTargetCommentId = useSetAtom(setTargetCommentIdAtom);
|
||||||
const clearTargetCommentId = useSetAtom(clearTargetCommentIdAtom);
|
const clearTargetCommentId = useSetAtom(clearTargetCommentIdAtom);
|
||||||
const closeReportPanel = useSetAtom(closeReportPanelAtom);
|
const closeReportPanel = useSetAtom(closeReportPanelAtom);
|
||||||
|
|
@ -951,6 +966,7 @@ export default function NewChatPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error("[NewChatPage] Chat error:", error);
|
console.error("[NewChatPage] Chat error:", error);
|
||||||
|
const premiumQuotaAlertMessage = getPinnedPremiumQuotaErrorMessage(error);
|
||||||
|
|
||||||
// Track chat error
|
// Track chat error
|
||||||
trackChatError(
|
trackChatError(
|
||||||
|
|
@ -959,7 +975,15 @@ export default function NewChatPage() {
|
||||||
error instanceof Error ? error.message : "Unknown error"
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
);
|
);
|
||||||
|
|
||||||
toast.error("Failed to get response. Please try again.");
|
if (premiumQuotaAlertMessage) {
|
||||||
|
setPremiumAlertForThread({
|
||||||
|
threadId: currentThreadId,
|
||||||
|
message: premiumQuotaAlertMessage,
|
||||||
|
});
|
||||||
|
toast.error(PINNED_PREMIUM_QUOTA_MESSAGE);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to get response. Please try again.");
|
||||||
|
}
|
||||||
// Update assistant message with error
|
// Update assistant message with error
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((m) =>
|
prev.map((m) =>
|
||||||
|
|
@ -969,7 +993,9 @@ export default function NewChatPage() {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Sorry, there was an error. Please try again.",
|
text:
|
||||||
|
premiumQuotaAlertMessage ??
|
||||||
|
"Sorry, there was an error. Please try again.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -998,6 +1024,7 @@ export default function NewChatPage() {
|
||||||
pendingUserImageUrls,
|
pendingUserImageUrls,
|
||||||
setPendingUserImageUrls,
|
setPendingUserImageUrls,
|
||||||
toolsWithUI,
|
toolsWithUI,
|
||||||
|
setPremiumAlertForThread,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1257,13 +1284,29 @@ export default function NewChatPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error("[NewChatPage] Resume error:", error);
|
console.error("[NewChatPage] Resume error:", error);
|
||||||
toast.error("Failed to resume. Please try again.");
|
const premiumQuotaAlertMessage = getPinnedPremiumQuotaErrorMessage(error);
|
||||||
|
if (premiumQuotaAlertMessage) {
|
||||||
|
setPremiumAlertForThread({
|
||||||
|
threadId: resumeThreadId,
|
||||||
|
message: premiumQuotaAlertMessage,
|
||||||
|
});
|
||||||
|
toast.error(PINNED_PREMIUM_QUOTA_MESSAGE);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to resume. Please try again.");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pendingInterrupt, messages, searchSpaceId, tokenUsageStore, toolsWithUI]
|
[
|
||||||
|
pendingInterrupt,
|
||||||
|
messages,
|
||||||
|
searchSpaceId,
|
||||||
|
tokenUsageStore,
|
||||||
|
toolsWithUI,
|
||||||
|
setPremiumAlertForThread,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1584,18 +1627,34 @@ export default function NewChatPage() {
|
||||||
}
|
}
|
||||||
batcher.dispose();
|
batcher.dispose();
|
||||||
console.error("[NewChatPage] Regeneration error:", error);
|
console.error("[NewChatPage] Regeneration error:", error);
|
||||||
|
const premiumQuotaAlertMessage = getPinnedPremiumQuotaErrorMessage(error);
|
||||||
trackChatError(
|
trackChatError(
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
threadId,
|
threadId,
|
||||||
error instanceof Error ? error.message : "Unknown error"
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
);
|
);
|
||||||
toast.error("Failed to regenerate response. Please try again.");
|
if (premiumQuotaAlertMessage) {
|
||||||
|
setPremiumAlertForThread({
|
||||||
|
threadId,
|
||||||
|
message: premiumQuotaAlertMessage,
|
||||||
|
});
|
||||||
|
toast.error(PINNED_PREMIUM_QUOTA_MESSAGE);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to regenerate response. Please try again.");
|
||||||
|
}
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((m) =>
|
prev.map((m) =>
|
||||||
m.id === assistantMsgId
|
m.id === assistantMsgId
|
||||||
? {
|
? {
|
||||||
...m,
|
...m,
|
||||||
content: [{ type: "text", text: "Sorry, there was an error. Please try again." }],
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
premiumQuotaAlertMessage ??
|
||||||
|
"Sorry, there was an error. Please try again.",
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
: m
|
: m
|
||||||
)
|
)
|
||||||
|
|
@ -1605,7 +1664,15 @@ export default function NewChatPage() {
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[threadId, searchSpaceId, messages, disabledTools, tokenUsageStore, toolsWithUI]
|
[
|
||||||
|
threadId,
|
||||||
|
searchSpaceId,
|
||||||
|
messages,
|
||||||
|
disabledTools,
|
||||||
|
tokenUsageStore,
|
||||||
|
toolsWithUI,
|
||||||
|
setPremiumAlertForThread,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle editing a message - truncates history and regenerates with new query
|
// Handle editing a message - truncates history and regenerates with new query
|
||||||
|
|
|
||||||
33
surfsense_web/atoms/chat/premium-alert.atom.ts
Normal file
33
surfsense_web/atoms/chat/premium-alert.atom.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export type PremiumAlertState = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const premiumAlertByThreadAtom = atom<Record<number, PremiumAlertState>>({});
|
||||||
|
|
||||||
|
export const setPremiumAlertForThreadAtom = atom(
|
||||||
|
null,
|
||||||
|
(
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
payload: {
|
||||||
|
threadId: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const current = get(premiumAlertByThreadAtom);
|
||||||
|
set(premiumAlertByThreadAtom, {
|
||||||
|
...current,
|
||||||
|
[payload.threadId]: { message: payload.message },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clearPremiumAlertForThreadAtom = atom(null, (get, set, threadId: number) => {
|
||||||
|
const current = get(premiumAlertByThreadAtom);
|
||||||
|
if (!(threadId in current)) return;
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[threadId];
|
||||||
|
set(premiumAlertByThreadAtom, next);
|
||||||
|
});
|
||||||
|
|
@ -37,10 +37,13 @@ import {
|
||||||
toggleToolAtom,
|
toggleToolAtom,
|
||||||
} from "@/atoms/agent-tools/agent-tools.atoms";
|
} from "@/atoms/agent-tools/agent-tools.atoms";
|
||||||
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
||||||
import {
|
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
mentionedDocumentsAtom,
|
import { mentionedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||||
} from "@/atoms/chat/mentioned-documents.atom";
|
|
||||||
import { pendingUserImageDataUrlsAtom } from "@/atoms/chat/pending-user-images.atom";
|
import { pendingUserImageDataUrlsAtom } from "@/atoms/chat/pending-user-images.atom";
|
||||||
|
import {
|
||||||
|
clearPremiumAlertForThreadAtom,
|
||||||
|
premiumAlertByThreadAtom,
|
||||||
|
} from "@/atoms/chat/premium-alert.atom";
|
||||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||||
|
|
@ -134,6 +137,9 @@ const ThreadContent: FC = () => {
|
||||||
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
|
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
|
||||||
>
|
>
|
||||||
<ThreadScrollToBottom />
|
<ThreadScrollToBottom />
|
||||||
|
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
||||||
|
<PremiumQuotaPinnedAlert />
|
||||||
|
</AuiIf>
|
||||||
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
||||||
<Composer />
|
<Composer />
|
||||||
</AuiIf>
|
</AuiIf>
|
||||||
|
|
@ -143,6 +149,38 @@ const ThreadContent: FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PremiumQuotaPinnedAlert: FC = () => {
|
||||||
|
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||||
|
const alertsByThread = useAtomValue(premiumAlertByThreadAtom);
|
||||||
|
const clearPremiumAlertForThread = useSetAtom(clearPremiumAlertForThreadAtom);
|
||||||
|
|
||||||
|
const currentThreadId = currentThreadState?.id;
|
||||||
|
if (!currentThreadId) return null;
|
||||||
|
|
||||||
|
const alert = alertsByThread[currentThreadId];
|
||||||
|
if (!alert) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-2 rounded-2xl border border-amber-300/40 bg-amber-500/10 px-4 py-3 text-amber-50 shadow-lg backdrop-blur-sm">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-300" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium">Premium quota exhausted</p>
|
||||||
|
<p className="mt-1 text-xs text-amber-100/90">{alert.message}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex size-6 items-center justify-center rounded-md text-amber-200 transition-colors hover:bg-amber-200/20 hover:text-amber-50"
|
||||||
|
aria-label="Dismiss premium quota alert"
|
||||||
|
onClick={() => clearPremiumAlertForThread(currentThreadId)}
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ThreadScrollToBottom: FC = () => {
|
const ThreadScrollToBottom: FC = () => {
|
||||||
return (
|
return (
|
||||||
<ThreadPrimitive.ScrollToBottom asChild>
|
<ThreadPrimitive.ScrollToBottom asChild>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue