Merge remote-tracking branch 'origin/main' into vorflux/graph-perf-consolidation

This commit is contained in:
Vorflux AI 2026-03-28 06:04:17 +00:00
commit f4ab06ee15
3 changed files with 140 additions and 4 deletions

View file

@ -21,7 +21,6 @@ import {
Check,
ChevronDownIcon,
HistoryIcon,
PanelRightCloseIcon,
Plus,
SearchIcon,
SquarePenIcon,
@ -33,11 +32,12 @@ import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import ChatInput from "./input"
import ChatModelSelector from "./model-selector"
import { getNovaChatErrorCopy } from "@/lib/chat-stream-error"
import { GradientLogo, LogoBgGradient } from "@ui/assets/Logo"
import { useProject } from "@/stores"
import { useContainerTags } from "@/hooks/use-container-tags"
import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label"
import type { ModelId } from "@/lib/models"
import { modelNames, type ModelId } from "@/lib/models"
import { SuperLoader } from "../superloader"
import { UserMessage } from "./message/user-message"
import { AgentMessage } from "./message/agent-message"
@ -197,11 +197,32 @@ export function ChatSidebar({
return () => window.removeEventListener("scroll", handleWindowScroll)
}, [isMobile, viewMode])
const { messages, sendMessage, status, setMessages, stop } = useChat({
const {
messages,
sendMessage,
status,
setMessages,
stop,
error,
clearError,
} = useChat({
id: currentChatId ?? undefined,
transport: chatTransport,
})
const chatStreamError = useMemo(
() => (error ? getNovaChatErrorCopy(error, selectedModel) : null),
[error, selectedModel],
)
const handleModelChange = useCallback(
(modelId: ModelId) => {
setSelectedModel(modelId)
clearError()
},
[clearError],
)
useEffect(() => {
if (pendingThreadLoad && currentChatId === pendingThreadLoad.id) {
setMessages(pendingThreadLoad.messages)
@ -550,7 +571,7 @@ export function ChatSidebar({
<div className="flex items-center gap-3 min-w-0 flex-1 mr-2">
<ChatModelSelector
selectedModel={selectedModel}
onModelChange={setSelectedModel}
onModelChange={handleModelChange}
/>
<div
className={cn(
@ -826,6 +847,57 @@ export function ChatSidebar({
</div>
)}
{chatStreamError && (
<div
role="alert"
className={cn(
"mx-4 mb-2 rounded-lg bg-amber-950/40 px-3 py-2 text-sm text-amber-50/95",
dmSansClassName(),
)}
>
<div className="flex justify-between gap-2 items-start">
<div className="min-w-0">
<p className="font-medium leading-snug">
{chatStreamError.title}
</p>
<p className="text-xs text-amber-100/70 mt-1 leading-snug">
{chatStreamError.body}
</p>
{chatStreamError.otherModels.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{chatStreamError.otherModels.map((id) => {
const m = modelNames[id]
return (
<Button
key={id}
type="button"
size="sm"
variant="secondary"
className="h-8 text-xs rounded-full bg-[#141922] border-[#73737333] hover:bg-[#1a2230] text-white/90"
onClick={() => {
handleModelChange(id)
analytics.modelChanged({ model: id })
}}
>
Switch to {m.name} {m.version}
</Button>
)
})}
</div>
)}
</div>
<button
type="button"
onClick={clearError}
className="shrink-0 p-1 rounded-md text-amber-200/50 hover:text-amber-100/90 hover:bg-white/5"
aria-label="Dismiss error"
>
<XIcon className="size-4" />
</button>
</div>
</div>
)}
<ChatInput
value={input}
onChange={(e) => setInput(e.target.value)}

View file

@ -4,6 +4,24 @@
import * as Sentry from "@sentry/nextjs"
function sentryShouldDropExpectedNonActionableError(event: {
message?: string
exception?: { values?: Array<{ type?: string; value?: string }> }
}): boolean {
const patterns = [
/user location is not supported/i,
/this email domain is not allowed/i,
]
const matches = (s: string | undefined) =>
s != null && patterns.some((re) => re.test(s))
if (matches(event.message)) return true
for (const ex of event.exception?.values ?? []) {
if (matches(ex.value)) return true
}
return false
}
Sentry.init({
dsn: "https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904",
@ -25,6 +43,11 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
beforeSend(event) {
if (sentryShouldDropExpectedNonActionableError(event)) return null
return event
},
})
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart

View file

@ -0,0 +1,41 @@
import type { ModelId } from "@/lib/models"
const OTHER_MODELS: ModelId[] = ["gpt-5", "claude-sonnet-4.5"]
function flattenError(e: unknown): string {
if (e == null) return ""
if (typeof e === "string") return e
if (e instanceof Error) {
const parts = [e.message]
for (let c: unknown = e.cause; c instanceof Error; c = c.cause) {
parts.push(c.message)
}
return parts.join(" ")
}
return String(e)
}
export function getNovaChatErrorCopy(error: unknown, model: ModelId) {
const msg = flattenError(error)
const geminiGeo =
/user location is not supported/i.test(msg) ||
(/failed_precondition/i.test(msg) && /location is not supported/i.test(msg))
if (geminiGeo) {
return {
title: "This model isn't available in your region",
body: "Gemini can't be used from your location. Try another model above.",
otherModels: OTHER_MODELS.filter((id) => id !== model),
}
}
const body =
msg.length > 200
? `${msg.slice(0, 197).trim()}`
: msg || "Try again or switch models."
return {
title: "Something went wrong",
body,
otherModels: model === "gemini-2.5-pro" ? OTHER_MODELS : [],
}
}