From 98a0fc77a795908294833cf41faa12d24cdcf939 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 23 Jan 2026 18:06:59 -0500 Subject: [PATCH 1/2] chore: lint cleanup and dead code removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused imports, variables, and functions (redisRest._executeCommand, users._userDoc, files unused types) - Standardize import ordering: external packages first, then internal - Separate type-only imports from value imports - Convert Type[] to Array syntax consistently - Add eslint ignores for build output directories - Remove unnecessary eslint-disable comments - Remove redundant optional chaining where values are guaranteed - Fix variable shadowing (open→isDialogOpen, percentage→clickPercentage, index→partIndex, error→parsedError) - Fix SidebarProvider useEffect to avoid stale conditional initialization - Use function property syntax for interface methods (SpeechRecognition) --- apps/server/convex/files.ts | 1 - apps/server/convex/lib/redisRest.ts | 30 ----- apps/server/convex/users.ts | 20 --- apps/web/eslint.config.js | 5 +- .../components/ai-elements/conversation.tsx | 4 +- .../src/components/ai-elements/message.tsx | 4 +- .../components/ai-elements/model-selector.tsx | 2 +- .../components/ai-elements/prompt-input.tsx | 123 +++++++++--------- apps/web/src/components/app-sidebar.tsx | 55 ++++---- apps/web/src/components/chat-interface.tsx | 90 +++++++------ apps/web/src/components/command-palette.tsx | 11 +- apps/web/src/components/component-example.tsx | 60 ++++----- .../src/components/delete-account-modal.tsx | 2 +- apps/web/src/components/model-selector.tsx | 2 +- .../components/openrouter-connect-modal.tsx | 4 +- apps/web/src/components/start-screen.tsx | 6 +- apps/web/src/components/ui/badge.tsx | 3 +- apps/web/src/components/ui/button.tsx | 3 +- apps/web/src/components/ui/combobox.tsx | 2 +- apps/web/src/components/ui/command.tsx | 2 +- apps/web/src/components/ui/dialog.tsx | 2 +- apps/web/src/components/ui/dropdown-menu.tsx | 2 +- apps/web/src/components/ui/field.tsx | 5 +- apps/web/src/components/ui/input-group.tsx | 3 +- apps/web/src/components/ui/select.tsx | 2 +- apps/web/src/components/ui/sidebar.tsx | 17 +-- apps/web/src/components/ui/tabs.tsx | 3 +- apps/web/src/hooks/use-favorite-models.ts | 4 +- apps/web/src/hooks/use-persistent-chat.ts | 114 ++++++++-------- apps/web/src/lib/auth-client.tsx | 13 +- apps/web/src/lib/env.ts | 4 +- apps/web/src/lib/fuzzy-search.ts | 6 +- apps/web/src/lib/redis.ts | 25 ++-- apps/web/src/lib/utils.ts | 5 +- apps/web/src/providers/index.tsx | 8 +- apps/web/src/routes/__root.tsx | 2 +- apps/web/src/routes/about.tsx | 2 +- apps/web/src/routes/api/chat.ts | 10 +- apps/web/src/routes/auth/sign-in.tsx | 4 +- apps/web/src/routes/c/$chatId.tsx | 2 +- apps/web/src/routes/index.tsx | 6 +- apps/web/src/routes/openrouter/callback.tsx | 2 +- apps/web/src/routes/privacy.tsx | 2 +- apps/web/src/routes/settings.tsx | 22 ++-- apps/web/src/routes/terms.tsx | 2 +- apps/web/src/stores/model.ts | 54 ++++---- apps/web/src/stores/openrouter.ts | 8 +- apps/web/src/stores/pending-message.ts | 2 +- apps/web/src/stores/stream.ts | 2 +- apps/web/src/stores/ui.ts | 2 +- 50 files changed, 354 insertions(+), 410 deletions(-) diff --git a/apps/server/convex/files.ts b/apps/server/convex/files.ts index 63f64aea..a1522c28 100644 --- a/apps/server/convex/files.ts +++ b/apps/server/convex/files.ts @@ -1,6 +1,5 @@ import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; -import type { MutationCtx, QueryCtx } from "./_generated/server"; import type { Id } from "./_generated/dataModel"; import { createLogger } from "./lib/logger"; import { rateLimiter } from "./lib/rateLimiter"; diff --git a/apps/server/convex/lib/redisRest.ts b/apps/server/convex/lib/redisRest.ts index f4719932..f017b4a9 100644 --- a/apps/server/convex/lib/redisRest.ts +++ b/apps/server/convex/lib/redisRest.ts @@ -63,36 +63,6 @@ function getRedisCredentials(): { url: string; token: string } | null { return { url, token }; } -/** - * Execute a Redis command via REST API - * Note: Currently unused but kept for future single-command operations - */ -async function _executeCommand( - command: (string | number)[] -): Promise { - const creds = getRedisCredentials(); - if (!creds) { - throw new Error("Redis credentials not configured"); - } - - const response = await fetch(creds.url, { - method: "POST", - headers: { - Authorization: `Bearer ${creds.token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(command), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Redis command failed: ${response.status} - ${errorText}`); - } - - const data = await response.json(); - return data.result as T; -} - /** * Execute multiple Redis commands in a pipeline */ diff --git a/apps/server/convex/users.ts b/apps/server/convex/users.ts index 85acbc43..f6c4a2bc 100644 --- a/apps/server/convex/users.ts +++ b/apps/server/convex/users.ts @@ -9,26 +9,6 @@ import { getProfileByUserId, getOrCreateProfile } from "./lib/profiles"; import { authComponent } from "./auth"; import { components } from "./_generated/api"; -// User document validator with all fields including fileUploadCount -// Note: Kept for potential future use (e.g., admin queries that need raw user data) -const _userDoc = v.object({ - _id: v.id("users"), - _creationTime: v.number(), - externalId: v.string(), - email: v.optional(v.string()), - name: v.optional(v.string()), - avatarUrl: v.optional(v.string()), - encryptedOpenRouterKey: v.optional(v.string()), - fileUploadCount: v.optional(v.number()), - // Ban fields - banned: v.optional(v.boolean()), - bannedAt: v.optional(v.number()), - banReason: v.optional(v.string()), - banExpiresAt: v.optional(v.number()), - createdAt: v.number(), - updatedAt: v.number(), -}); - // User with profile data (for backwards-compatible responses) // Includes merged profile data that prefers profile over user during migration const userWithProfileDoc = v.object({ diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js index 676b32a8..7252afbc 100644 --- a/apps/web/eslint.config.js +++ b/apps/web/eslint.config.js @@ -2,4 +2,7 @@ import { tanstackConfig } from '@tanstack/eslint-config' -export default [...tanstackConfig] +export default [ + { ignores: ['.output/**', 'dist/**', '.vinxi/**', '*.config.js'] }, + ...tanstackConfig, +] diff --git a/apps/web/src/components/ai-elements/conversation.tsx b/apps/web/src/components/ai-elements/conversation.tsx index 094edd3b..c1e6f463 100644 --- a/apps/web/src/components/ai-elements/conversation.tsx +++ b/apps/web/src/components/ai-elements/conversation.tsx @@ -12,10 +12,10 @@ "use client"; -import { cn } from "@/lib/utils"; import { ArrowDownIcon } from "lucide-react"; -import type { ComponentProps, RefObject } from "react"; import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; +import type { ComponentProps, RefObject } from "react"; +import { cn } from "@/lib/utils"; // ============================================================================ // Context for scroll state diff --git a/apps/web/src/components/ai-elements/message.tsx b/apps/web/src/components/ai-elements/message.tsx index fef7165b..a52e35c5 100644 --- a/apps/web/src/components/ai-elements/message.tsx +++ b/apps/web/src/components/ai-elements/message.tsx @@ -9,10 +9,10 @@ "use client"; -import { cn } from "@/lib/utils"; -import type { ComponentProps, ReactNode } from "react"; import { createContext, useContext } from "react"; import { Streamdown } from "streamdown"; +import type { ComponentProps, ReactNode } from "react"; +import { cn } from "@/lib/utils"; // ============================================================================ // Context diff --git a/apps/web/src/components/ai-elements/model-selector.tsx b/apps/web/src/components/ai-elements/model-selector.tsx index ca47f0dd..af235dec 100644 --- a/apps/web/src/components/ai-elements/model-selector.tsx +++ b/apps/web/src/components/ai-elements/model-selector.tsx @@ -1,3 +1,4 @@ +import type { ComponentProps, ReactNode } from "react"; import { Command, CommandDialog, @@ -11,7 +12,6 @@ import { } from "@/components/ui/command"; import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; -import type { ComponentProps, ReactNode } from "react"; export type ModelSelectorProps = ComponentProps; diff --git a/apps/web/src/components/ai-elements/prompt-input.tsx b/apps/web/src/components/ai-elements/prompt-input.tsx index b6c59b31..706f5010 100644 --- a/apps/web/src/components/ai-elements/prompt-input.tsx +++ b/apps/web/src/components/ai-elements/prompt-input.tsx @@ -1,5 +1,41 @@ "use client"; +import { + CornerDownLeftIcon, + ImageIcon, + Loader2Icon, + MicIcon, + PaperclipIcon, + PlusIcon, + SquareIcon, + XIcon, +} from "lucide-react"; +import { motion } from "motion/react"; +import { nanoid } from "nanoid"; +import { + + + Children, + + + + + Fragment, + + + + + + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState +} from "react"; +import type { ChatStatus, FileUIPart } from "ai"; +import type {ChangeEvent, ChangeEventHandler, ClipboardEventHandler, ComponentProps, FormEvent, FormEventHandler, HTMLAttributes, KeyboardEventHandler, PropsWithChildren, ReactNode, RefObject} from "react"; import { Button } from "@/components/ui/button"; import { Command, @@ -31,49 +67,14 @@ import { SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; -import type { ChatStatus, FileUIPart } from "ai"; -import { - CornerDownLeftIcon, - ImageIcon, - Loader2Icon, - MicIcon, - PaperclipIcon, - PlusIcon, - SquareIcon, - XIcon, -} from "lucide-react"; -import { motion } from "motion/react"; -import { nanoid } from "nanoid"; -import { - type ChangeEvent, - type ChangeEventHandler, - Children, - type ClipboardEventHandler, - type ComponentProps, - createContext, - type FormEvent, - type FormEventHandler, - Fragment, - type HTMLAttributes, - type KeyboardEventHandler, - type PropsWithChildren, - type ReactNode, - type RefObject, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; // ============================================================================ // Provider Context & Types // ============================================================================ export type AttachmentsContext = { - files: (FileUIPart & { id: string })[]; - add: (files: File[] | FileList) => void; + files: Array; + add: (files: Array | FileList) => void; remove: (id: string) => void; clear: () => void; openFileDialog: () => void; @@ -138,11 +139,11 @@ export function PromptInputProvider({ const clearInput = useCallback(() => setTextInput(""), []); // ----- attachments state (global when wrapped) - const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]); + const [attachmentFiles, setAttachmentFiles] = useState>([]); const fileInputRef = useRef(null); const openRef = useRef<() => void>(() => {}); - const add = useCallback((files: File[] | FileList) => { + const add = useCallback((files: Array | FileList) => { const incoming = Array.from(files); if (incoming.length === 0) { return; @@ -198,7 +199,7 @@ export function PromptInputProvider({ }, []); const openFileDialog = useCallback(() => { - openRef.current?.(); + openRef.current(); }, []); const attachments = useMemo( @@ -272,7 +273,7 @@ export function PromptInputAttachment({ data, className, ...props }: PromptInput const filename = data.filename || ""; - const mediaType = data.mediaType?.startsWith("image/") && data.url ? "image" : "file"; + const mediaType = data.mediaType.startsWith("image/") && data.url ? "image" : "file"; const isImage = mediaType === "image"; const attachmentLabel = filename || (isImage ? "Image" : "Attachment"); @@ -475,7 +476,7 @@ export const PromptInputAttachmentButton = ({ export type PromptInputMessage = { text: string; - files: FileUIPart[]; + files: Array; }; export type PromptInputProps = Omit, "onSubmit" | "onError"> & { @@ -517,7 +518,7 @@ export const PromptInput = ({ const formRef = useRef(null); // ----- Local attachments (only used when no provider) - const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]); + const [items, setItems] = useState>([]); const files = usingProvider ? controller.attachments.files : items; // Keep a ref to files for cleanup on unmount (avoids stale closure) @@ -551,7 +552,7 @@ export const PromptInput = ({ ); const addLocal = useCallback( - (fileList: File[] | FileList) => { + (fileList: Array | FileList) => { const incoming = Array.from(fileList); const accepted = incoming.filter((f) => matchesAccept(f)); if (incoming.length && accepted.length === 0) { @@ -581,7 +582,7 @@ export const PromptInput = ({ message: "Too many files. Some were not added.", }); } - const next: (FileUIPart & { id: string })[] = []; + const next: Array = []; for (const file of capped) { next.push({ id: nanoid(), @@ -650,12 +651,12 @@ export const PromptInput = ({ if (globalDrop) return; // when global drop is on, let the document-level handler own drops const onDragOver = (e: DragEvent) => { - if (e.dataTransfer?.types?.includes("Files")) { + if (e.dataTransfer?.types.includes("Files")) { e.preventDefault(); } }; const onDrop = (e: DragEvent) => { - if (e.dataTransfer?.types?.includes("Files")) { + if (e.dataTransfer?.types.includes("Files")) { e.preventDefault(); } if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { @@ -674,12 +675,12 @@ export const PromptInput = ({ if (!globalDrop) return; const onDragOver = (e: DragEvent) => { - if (e.dataTransfer?.types?.includes("Files")) { + if (e.dataTransfer?.types.includes("Files")) { e.preventDefault(); } }; const onDrop = (e: DragEvent) => { - if (e.dataTransfer?.types?.includes("Files")) { + if (e.dataTransfer?.types.includes("Files")) { e.preventDefault(); } if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { @@ -702,7 +703,7 @@ export const PromptInput = ({ } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps -- cleanup only on unmount; filesRef always current + // cleanup only on unmount; filesRef always current [usingProvider], ); @@ -772,7 +773,7 @@ export const PromptInput = ({ return item; }), ) - .then((convertedFiles: FileUIPart[]) => { + .then((convertedFiles: Array) => { try { const result = onSubmit({ text, files: convertedFiles }, event); @@ -879,13 +880,9 @@ export const PromptInputTextarea = ({ }; const handlePaste: ClipboardEventHandler = (event) => { - const items = event.clipboardData?.items; - - if (!items) { - return; - } + const items = event.clipboardData.items; - const files: File[] = []; + const files: Array = []; for (const item of items) { if (item.kind === "file") { @@ -1056,8 +1053,8 @@ interface SpeechRecognition extends EventTarget { continuous: boolean; interimResults: boolean; lang: string; - start(): void; - stop(): void; + start: () => void; + stop: () => void; onstart: ((this: SpeechRecognition, ev: Event) => any) | null; onend: ((this: SpeechRecognition, ev: Event) => any) | null; onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null; @@ -1071,13 +1068,13 @@ interface SpeechRecognitionEvent extends Event { type SpeechRecognitionResultList = { readonly length: number; - item(index: number): SpeechRecognitionResult; + item: (index: number) => SpeechRecognitionResult; [index: number]: SpeechRecognitionResult; }; type SpeechRecognitionResult = { readonly length: number; - item(index: number): SpeechRecognitionAlternative; + item: (index: number) => SpeechRecognitionAlternative; [index: number]: SpeechRecognitionAlternative; isFinal: boolean; }; @@ -1122,7 +1119,7 @@ export const PromptInputSpeechButton = ({ typeof window !== "undefined" && ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) ) { - const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const SpeechRecognition = window.SpeechRecognition; const speechRecognition = new SpeechRecognition(); speechRecognition.continuous = true; @@ -1143,7 +1140,7 @@ export const PromptInputSpeechButton = ({ for (let i = event.resultIndex; i < event.results.length; i++) { const result = event.results[i]; if (result.isFinal) { - finalTranscript += result[0]?.transcript ?? ""; + finalTranscript += result[0].transcript; } } diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index e844c77e..dde6d92f 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -1,14 +1,29 @@ -import { useCallback, useEffect, useRef, useState, type MouseEvent } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "@tanstack/react-router"; import { useQuery } from "convex/react"; import { api } from "@server/convex/_generated/api"; +import { PencilIcon, SparklesIcon, Trash2Icon, XIcon } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "./ui/button"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "./ui/sidebar"; +import type {MouseEvent} from "react"; +import type { Id } from "@server/convex/_generated/dataModel"; import { useAuth } from "@/lib/auth-client"; import { convexClient } from "@/lib/convex"; import { useOpenRouterKey } from "@/stores/openrouter"; import { useProviderStore } from "@/stores/provider"; import { useChatTitleStore } from "@/stores/chat-title"; import { cn } from "@/lib/utils"; -import { Button } from "./ui/button"; import { AlertDialog, AlertDialogAction, @@ -19,21 +34,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuItem, - SidebarMenuButton, - useSidebar, -} from "./ui/sidebar"; -import { PlusIcon, ChatIcon, SidebarIcon, ChevronRightIcon, MenuIcon } from "@/components/icons"; -import { PencilIcon, SparklesIcon, Trash2Icon, XIcon } from "lucide-react"; -import { toast } from "sonner"; -import type { Id } from "@server/convex/_generated/dataModel"; +import { ChatIcon, ChevronRightIcon, MenuIcon, PlusIcon, SidebarIcon } from "@/components/icons"; const CHATS_CACHE_KEY = "openchat-chats-cache"; const CONTEXT_MENU_PADDING = 12; @@ -55,11 +56,11 @@ function ChatItemSkeleton({ delay = 0 }: { delay?: number }) { ); } -function groupChatsByTime(chats: ChatItem[]) { - const today: ChatItem[] = []; - const last7Days: ChatItem[] = []; - const last30Days: ChatItem[] = []; - const older: ChatItem[] = []; +function groupChatsByTime(chats: Array) { + const today: Array = []; + const last7Days: Array = []; + const last30Days: Array = []; + const older: Array = []; const now = Date.now(); const oneDayMs = 1000 * 60 * 60 * 24; @@ -83,12 +84,12 @@ function groupChatsByTime(chats: ChatItem[]) { interface ChatGroupProps { label: string; - chats: ChatItem[]; + chats: Array; currentChatId?: string; onChatClick: (chatId: string) => void; onChatContextMenu: (chatId: string, event: MouseEvent) => void; onQuickDelete: (chatId: string, event: React.MouseEvent) => void; - generatingChatIds: Record; + generatingChatIds: Partial>; editingChatId: string | null; editValue: string; onEditChange: (value: string) => void; @@ -221,7 +222,7 @@ export function AppSidebar() { convexClient && convexUser?._id ? { userId: convexUser._id } : "skip", ); - const cachedChatsRef = useRef(null); + const cachedChatsRef = useRef | null>(null); useEffect(() => { if (typeof window === "undefined") return; @@ -678,8 +679,8 @@ export function AppSidebar() { )} { - if (!open) setDeleteChatId(null); + onOpenChange={(isDialogOpen) => { + if (!isDialogOpen) setDeleteChatId(null); }} > prev + 1); setIsRetrying(false); - // Call the retry function - onRetry?.(); + onRetry(); }; // Get human-readable error title based on code @@ -278,11 +277,11 @@ interface ChainOfThoughtStep { // This preserves the exact stream order // Each reasoning part is its own step (not merged) so they can collapse independently function buildChainOfThoughtSteps(parts: Array): { - steps: ChainOfThoughtStep[]; + steps: Array; isAnyStreaming: boolean; hasTextContent: boolean; } { - const steps: ChainOfThoughtStep[] = []; + const steps: Array = []; let isAnyStreaming = false; let hasTextContent = false; @@ -340,7 +339,7 @@ function buildChainOfThoughtSteps(parts: Array): { // Chain of Thought Component - Multi-step reasoning visualization interface ChainOfThoughtProps { - steps: ChainOfThoughtStep[]; + steps: Array; isStreaming?: boolean; hasTextContent?: boolean; // Whether the message has text content (for auto-collapse) } @@ -366,7 +365,6 @@ function ChainOfThought({ hasAutoCollapsedRef.current = false; } else if ( wasStreamingRef.current && - !isStreaming && hasTextContent && !hasAutoCollapsedRef.current ) { @@ -515,7 +513,7 @@ function ChainOfThoughtStepItem({ step }: { step: ChainOfThoughtStep }) { {/* Expand indicator - show for reasoning with content OR tool with output */} - {(step.content || (step.type === "tool" && step.toolOutput)) && ( + {Boolean(step.content || (step.type === "tool" && step.toolOutput)) && ( @@ -583,7 +581,7 @@ function replaceUtmSource(url: string): string { function SearchResultsDisplay({ results, isExpanded }: { results: unknown; isExpanded: boolean }) { // Parse the results - handle different structures from various search tools // Could be: array directly, { results: [...] }, { data: [...] }, etc. - let searchResults: any[] = []; + let searchResults: Array = []; if (Array.isArray(results)) { searchResults = results; @@ -663,7 +661,7 @@ interface ReasoningSliderProps { onChange: (value: ReasoningEffort) => void; } -const EFFORT_OPTIONS: ReasoningEffort[] = ["none", "low", "medium", "high"]; +const EFFORT_OPTIONS: Array = ["none", "low", "medium", "high"]; const EFFORT_LABELS: Record = { none: "None", low: "Low", @@ -682,8 +680,8 @@ function ReasoningSlider({ value, onChange }: ReasoningSliderProps) { const handleTrackClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; - const percentage = x / rect.width; - const index = Math.round(percentage * (EFFORT_OPTIONS.length - 1)); + const clickPercentage = x / rect.width; + const index = Math.round(clickPercentage * (EFFORT_OPTIONS.length - 1)); onChange(EFFORT_OPTIONS[Math.max(0, Math.min(index, EFFORT_OPTIONS.length - 1))]); }; @@ -760,7 +758,7 @@ function ModelConfigPopover({ disabled }: ModelConfigPopoverProps) { const isMobile = useIsMobile(); const getBadgeText = () => { - const parts: string[] = []; + const parts: Array = []; if (reasoningEffort !== "none") { parts.push(reasoningEffort.toUpperCase()); } @@ -1290,7 +1288,7 @@ function ChatInterfaceContent({ if (isTextareaFocused) { textarea.blur(); // Also blur the document to ensure we're not stuck in the input - (document.activeElement as HTMLElement)?.blur?.(); + (document.activeElement as HTMLElement).blur(); } else { textarea.focus(); } @@ -1424,19 +1422,19 @@ function ChatInterfaceContent({ )} {/* Text content */} - {textParts.map((part, index) => ( + {textParts.map((part, partIndex) => ( {part.text || ""} ))} {/* File attachments */} - {fileParts.map((part, index) => ( + {fileParts.map((part, partIndex) => ( void; - keywords?: string[]; + keywords?: Array; } // Mock recent chats data @@ -74,8 +74,8 @@ export function CommandPalette() { const inputRef = useRef(null); const listRef = useRef(null); - const allItems = useMemo(() => { - const actions: CommandItem[] = [ + const allItems = useMemo>(() => { + const actions: Array = [ { id: "new-chat", type: "action", @@ -111,7 +111,7 @@ export function CommandPalette() { }, ]; - const chats: CommandItem[] = mockChats.map((chat) => ({ + const chats: Array = mockChats.map((chat) => ({ id: `chat-${chat.id}`, type: "chat", title: chat.title, @@ -166,7 +166,6 @@ export function CommandPalette() { } // NOTE: isClosing intentionally omitted from deps to prevent the effect // from re-running and clearing the close timer when isClosing changes - // eslint-disable-next-line react-hooks/exhaustive-deps }, [commandPaletteOpen, isVisible]); // Focus input when opening diff --git a/apps/web/src/components/component-example.tsx b/apps/web/src/components/component-example.tsx index c869b4b7..c8fdcff0 100644 --- a/apps/web/src/components/component-example.tsx +++ b/apps/web/src/components/component-example.tsx @@ -1,5 +1,35 @@ import * as React from "react"; +import { + BellIcon, + BluetoothIcon, + CreditCardIcon, + DownloadIcon, + EyeIcon, + FileCodeIcon, + FileIcon, + FileTextIcon, + FolderIcon, + FolderOpenIcon, + FolderSearchIcon, + HelpCircleIcon, + KeyboardIcon, + LanguagesIcon, + LayoutIcon, + LogOutIcon, + MailIcon, + MonitorIcon, + MoonIcon, + MoreHorizontalIcon, + MoreVerticalIcon, + PaletteIcon, + PlusIcon, + SaveIcon, + SettingsIcon, + ShieldIcon, + SunIcon, + UserIcon, +} from "lucide-react"; import { Example, ExampleWrapper } from "@/components/example"; import { AlertDialog, @@ -60,36 +90,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; -import { - PlusIcon, - BluetoothIcon, - MoreVerticalIcon, - FileIcon, - FolderIcon, - FolderOpenIcon, - FileCodeIcon, - MoreHorizontalIcon, - FolderSearchIcon, - SaveIcon, - DownloadIcon, - EyeIcon, - LayoutIcon, - PaletteIcon, - SunIcon, - MoonIcon, - MonitorIcon, - UserIcon, - CreditCardIcon, - SettingsIcon, - KeyboardIcon, - LanguagesIcon, - BellIcon, - MailIcon, - ShieldIcon, - HelpCircleIcon, - FileTextIcon, - LogOutIcon, -} from "lucide-react"; export function ComponentExample() { return ( diff --git a/apps/web/src/components/delete-account-modal.tsx b/apps/web/src/components/delete-account-modal.tsx index 383343bb..366e8b0f 100644 --- a/apps/web/src/components/delete-account-modal.tsx +++ b/apps/web/src/components/delete-account-modal.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { useMutation } from "convex/react"; import { api } from "@server/convex/_generated/api"; +import { AlertTriangleIcon } from "lucide-react"; import type { Id } from "@server/convex/_generated/dataModel"; import { signOut } from "@/lib/auth-client"; import { @@ -15,7 +16,6 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { AlertTriangleIcon } from "lucide-react"; interface DeleteAccountModalProps { userId: Id<"users">; diff --git a/apps/web/src/components/model-selector.tsx b/apps/web/src/components/model-selector.tsx index 5cdd3068..f926a220 100644 --- a/apps/web/src/components/model-selector.tsx +++ b/apps/web/src/components/model-selector.tsx @@ -2,7 +2,7 @@ import { useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, use import { createPortal, flushSync } from "react-dom"; import type { Model } from "@/stores/model"; import { cn } from "@/lib/utils"; -import { getModelById, useModels, useModelStore } from "@/stores/model"; +import { getModelById, useModelStore, useModels } from "@/stores/model"; import { useFavoriteModels } from "@/hooks/use-favorite-models"; import { CheckIcon, ChevronDownIcon, SearchIcon } from "@/components/icons"; diff --git a/apps/web/src/components/openrouter-connect-modal.tsx b/apps/web/src/components/openrouter-connect-modal.tsx index 2440c874..7493593d 100644 --- a/apps/web/src/components/openrouter-connect-modal.tsx +++ b/apps/web/src/components/openrouter-connect-modal.tsx @@ -7,11 +7,11 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; +import { CheckIcon, ExternalLinkIcon, KeyIcon, XIcon } from "lucide-react"; import { Button } from "./ui/button"; import { useOpenRouterKey } from "@/stores/openrouter"; import { cn } from "@/lib/utils"; -import { XIcon, ExternalLinkIcon, CheckIcon, KeyIcon } from "lucide-react"; interface OpenRouterConnectModalProps { open: boolean; diff --git a/apps/web/src/components/start-screen.tsx b/apps/web/src/components/start-screen.tsx index 5cb0b7a9..6ac15ae1 100644 --- a/apps/web/src/components/start-screen.tsx +++ b/apps/web/src/components/start-screen.tsx @@ -11,9 +11,9 @@ "use client"; import { useState } from "react"; +import { BookOpenIcon, ChevronRightIcon, CodeIcon, CompassIcon, PenLineIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { useAuth } from "@/lib/auth-client"; -import { PenLineIcon, CompassIcon, CodeIcon, BookOpenIcon, ChevronRightIcon } from "lucide-react"; // ============================================================================ // Types @@ -36,7 +36,7 @@ interface CategoryConfig { // Constants // ============================================================================ -const CATEGORIES: CategoryConfig[] = [ +const CATEGORIES: Array = [ { id: "create", label: "Create", icon: }, { id: "explore", label: "Explore", icon: }, { id: "code", label: "Code", icon: }, @@ -44,7 +44,7 @@ const CATEGORIES: CategoryConfig[] = [ ]; // Category-specific suggestions (shown when that category is selected) -const SUGGESTIONS_BY_CATEGORY: Record = { +const SUGGESTIONS_BY_CATEGORY: Record> = { create: [ "Write a short story about a robot discovering emotions", "Help me outline a sci-fi novel set in a post-apocalyptic world", diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx index 92a34812..b9dbb57b 100644 --- a/apps/web/src/components/ui/badge.tsx +++ b/apps/web/src/components/ui/badge.tsx @@ -1,6 +1,7 @@ import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type {VariantProps} from "class-variance-authority"; import { cn } from "@/lib/utils"; diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 2f1e53d4..3c1a4886 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -1,5 +1,6 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type {VariantProps} from "class-variance-authority"; import { cn } from "@/lib/utils"; diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index 76acffd3..8af27d7e 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { Combobox as ComboboxPrimitive } from "@base-ui/react"; +import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { @@ -11,7 +12,6 @@ import { InputGroupButton, InputGroupInput, } from "@/components/ui/input-group"; -import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"; const Combobox = ComboboxPrimitive.Root; diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index d0dc6c94..24047b14 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import { Command as CommandPrimitive } from "cmdk"; +import { CheckIcon, SearchIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Dialog, @@ -12,7 +13,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"; -import { SearchIcon, CheckIcon } from "lucide-react"; function Command({ className, ...props }: React.ComponentProps) { return ( diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index a3807137..70383237 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -3,9 +3,9 @@ import * as React from "react"; import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; +import { XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import { XIcon } from "lucide-react"; function Dialog({ ...props }: DialogPrimitive.Root.Props) { return ; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx index bd94738d..129ca0f5 100644 --- a/apps/web/src/components/ui/dropdown-menu.tsx +++ b/apps/web/src/components/ui/dropdown-menu.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { Menu as MenuPrimitive } from "@base-ui/react/menu"; +import { CheckIcon, ChevronRightIcon } from "lucide-react"; import { cn } from "@/lib/utils"; -import { ChevronRightIcon, CheckIcon } from "lucide-react"; function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { return ; diff --git a/apps/web/src/components/ui/field.tsx b/apps/web/src/components/ui/field.tsx index eb838636..ea9d5c4c 100644 --- a/apps/web/src/components/ui/field.tsx +++ b/apps/web/src/components/ui/field.tsx @@ -1,5 +1,6 @@ import { useMemo } from "react"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type {VariantProps} from "class-variance-authority"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; @@ -181,7 +182,7 @@ function FieldError({ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()]; - if (uniqueErrors?.length == 1) { + if (uniqueErrors.length == 1) { return uniqueErrors[0]?.message; } diff --git a/apps/web/src/components/ui/input-group.tsx b/apps/web/src/components/ui/input-group.tsx index f503466e..6f4bb2de 100644 --- a/apps/web/src/components/ui/input-group.tsx +++ b/apps/web/src/components/ui/input-group.tsx @@ -1,5 +1,6 @@ import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type {VariantProps} from "class-variance-authority"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx index 69362854..f6866964 100644 --- a/apps/web/src/components/ui/select.tsx +++ b/apps/web/src/components/ui/select.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { Select as SelectPrimitive } from "@base-ui/react/select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { cn } from "@/lib/utils"; -import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"; const Select = SelectPrimitive.Root; diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 11208a51..03639ad9 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -1,10 +1,11 @@ "use client"; import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils"; +import { cva } from "class-variance-authority"; import { Button } from "./button"; import { Separator } from "./separator"; +import type {VariantProps} from "class-variance-authority"; +import { cn } from "@/lib/utils"; import { useUIStore } from "@/stores/ui"; // ============================================================================ @@ -48,19 +49,13 @@ function SidebarProvider({ const [isMobile, setIsMobile] = React.useState(false); - // Initialize from store or defaults + // Initialize from store or defaults on first render React.useEffect(() => { - if (sidebarOpen === undefined) { - setSidebarOpen(defaultOpen); - } - if (sidebarCollapsed === undefined) { - setSidebarCollapsed(defaultCollapsed); - } + setSidebarOpen(defaultOpen); + setSidebarCollapsed(defaultCollapsed); }, [ defaultOpen, defaultCollapsed, - sidebarOpen, - sidebarCollapsed, setSidebarOpen, setSidebarCollapsed, ]); diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx index a869b07b..4e27c4ce 100644 --- a/apps/web/src/components/ui/tabs.tsx +++ b/apps/web/src/components/ui/tabs.tsx @@ -1,5 +1,6 @@ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type {VariantProps} from "class-variance-authority"; import { cn } from "@/lib/utils"; diff --git a/apps/web/src/hooks/use-favorite-models.ts b/apps/web/src/hooks/use-favorite-models.ts index 83b8323e..3e73db4a 100644 --- a/apps/web/src/hooks/use-favorite-models.ts +++ b/apps/web/src/hooks/use-favorite-models.ts @@ -1,7 +1,7 @@ -import { useQuery, useMutation } from "convex/react"; +import { useMutation, useQuery } from "convex/react"; import { api } from "@server/convex/_generated/api"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useAuth } from "@/lib/auth-client"; -import { useState, useCallback, useEffect, useRef } from "react"; export const DEFAULT_FAVORITES = [ "anthropic/claude-opus-4.5", diff --git a/apps/web/src/hooks/use-persistent-chat.ts b/apps/web/src/hooks/use-persistent-chat.ts index 97f4d237..1966ee9e 100644 --- a/apps/web/src/hooks/use-persistent-chat.ts +++ b/apps/web/src/hooks/use-persistent-chat.ts @@ -1,16 +1,16 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useAction, useMutation, useQuery } from "convex/react"; import { api } from "@server/convex/_generated/api"; +import { toast } from "sonner"; import type { Id } from "@server/convex/_generated/dataModel"; +import type { UIMessage } from "ai"; import { useAuth } from "@/lib/auth-client"; import { useModelStore } from "@/stores/model"; import { useOpenRouterKey } from "@/stores/openrouter"; import { useProviderStore } from "@/stores/provider"; import { useChatTitleStore } from "@/stores/chat-title"; import { useStreamStore } from "@/stores/stream"; -import { toast } from "sonner"; import { analytics } from "@/lib/analytics"; -import type { UIMessage } from "ai"; interface ChatFileAttachment { type: "file"; @@ -25,8 +25,8 @@ export interface UsePersistentChatOptions { } export interface UsePersistentChatReturn { - messages: UIMessage[]; - sendMessage: (message: { text: string; files?: ChatFileAttachment[] }) => Promise; + messages: Array; + sendMessage: (message: { text: string; files?: Array }) => Promise; status: "ready" | "submitted" | "streaming" | "error"; error: Error | undefined; stop: () => void; @@ -84,7 +84,7 @@ export function usePersistentChat({ const chatTitleLength = useChatTitleStore((s) => s.length); const setTitleGenerating = useChatTitleStore((s) => s.setGenerating); - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState>([]); const [status, setStatus] = useState<"ready" | "submitted" | "streaming" | "error">("ready"); const [error, setError] = useState(undefined); const [currentChatId, setCurrentChatId] = useState(chatId ?? null); @@ -146,12 +146,12 @@ export function usePersistentChat({ } const lastPrev = prevMessages[prevMessages.length - 1]; - const isLastPrevStreaming = lastPrev?.id.startsWith("resume-") || - (lastPrev?.role === "assistant" && !messagesResult.find(m => m._id === lastPrev.id)); + const isLastPrevStreaming = lastPrev.id.startsWith("resume-") || + (lastPrev.role === "assistant" && !messagesResult.find(m => m._id === lastPrev.id)); if (isLastPrevStreaming && convexMessages.length > 0) { const lastConvex = convexMessages[convexMessages.length - 1]; - if (lastConvex?.role === "assistant") { + if (lastConvex.role === "assistant") { return [ ...convexMessages.slice(0, -1), { ...lastConvex, id: lastPrev.id } @@ -184,52 +184,50 @@ export function usePersistentChat({ return; } - if (activeStreamJob.status === "running" || activeStreamJob.status === "pending") { - const streamId = activeStreamJob.messageId; - const jobContent = activeStreamJob.content || ""; - const jobReasoning = activeStreamJob.reasoning || ""; + const streamId = activeStreamJob.messageId; + const jobContent = activeStreamJob.content || ""; + const jobReasoning = activeStreamJob.reasoning || ""; - if (status !== "streaming" && status !== "submitted") { - console.log("[BackgroundStream] Detected running stream job, resuming UI..."); - setStatus("streaming"); - useStreamStore.getState().setResuming(); - } + if (status !== "streaming" && status !== "submitted") { + console.log("[BackgroundStream] Detected running stream job, resuming UI..."); + setStatus("streaming"); + useStreamStore.getState().setResuming(); + } - if (!streamingRef.current || streamingRef.current.id !== streamId) { - streamingRef.current = { id: streamId, content: jobContent, reasoning: jobReasoning }; + if (!streamingRef.current || streamingRef.current.id !== streamId) { + streamingRef.current = { id: streamId, content: jobContent, reasoning: jobReasoning }; + + setMessages((prev) => { + if (prev.find(m => m.id === streamId)) return prev; + return [ + ...prev, + { id: streamId, role: "assistant" as const, parts: [{ type: "text" as const, text: jobContent }] }, + ]; + }); + } else if ( + streamingRef.current.content !== jobContent || + streamingRef.current.reasoning !== jobReasoning + ) { + streamingRef.current.content = jobContent; + streamingRef.current.reasoning = jobReasoning; + + setMessages((prev) => { + const idx = prev.findIndex((m) => m.id === streamId); + if (idx < 0) return prev; - setMessages((prev) => { - if (prev.find(m => m.id === streamId)) return prev; - return [ - ...prev, - { id: streamId, role: "assistant" as const, parts: [{ type: "text" as const, text: jobContent }] }, - ]; - }); - } else if ( - streamingRef.current.content !== jobContent || - streamingRef.current.reasoning !== jobReasoning - ) { - streamingRef.current.content = jobContent; - streamingRef.current.reasoning = jobReasoning; + const currentText = prev[idx].parts.find(p => p.type === "text"); + if (currentText && "text" in currentText && currentText.text === jobContent) { + return prev; + } - setMessages((prev) => { - const idx = prev.findIndex((m) => m.id === streamId); - if (idx < 0) return prev; - - const currentText = prev[idx].parts?.find(p => p.type === "text"); - if (currentText && "text" in currentText && currentText.text === jobContent) { - return prev; - } - - const parts: UIMessage["parts"] = []; - if (jobReasoning) parts.push({ type: "reasoning", text: jobReasoning }); - parts.push({ type: "text", text: jobContent }); - - const updated = [...prev]; - updated[idx] = { ...updated[idx], parts }; - return updated; - }); - } + const parts: UIMessage["parts"] = []; + if (jobReasoning) parts.push({ type: "reasoning", text: jobReasoning }); + parts.push({ type: "text", text: jobContent }); + + const updated = [...prev]; + updated[idx] = { ...updated[idx], parts }; + return updated; + }); } }, [activeStreamJob, status]); @@ -238,7 +236,7 @@ export function usePersistentChat({ const isUserLoading = !!(user?.id && convexUser === undefined); const handleSendMessage = useCallback( - async (message: { text: string; files?: ChatFileAttachment[] }) => { + async (message: { text: string; files?: Array }) => { if (!convexUserId) { if (isUserLoading) { toast.error("Please wait", { description: "Setting up your account." }); @@ -295,7 +293,7 @@ export function usePersistentChat({ try { const allMsgs = messages.map((m) => { - const textPart = m.parts?.find((p): p is { type: "text"; text: string } => p.type === "text"); + const textPart = m.parts.find((p): p is { type: "text"; text: string } => p.type === "text"); return { role: m.role, content: textPart?.text || "" }; }); allMsgs.push({ role: "user", content: message.text }); @@ -309,7 +307,7 @@ export function usePersistentChat({ apiKey: activeProvider === "openrouter" && apiKey ? apiKey : undefined, messages: allMsgs, options: { - reasoningEffort: reasoningEffort || undefined, + reasoningEffort, enableWebSearch: webSearchEnabled, maxSteps, }, @@ -350,11 +348,11 @@ export function usePersistentChat({ return { status: "empty" } as const; } catch (err) { console.warn("[Chat] Title generation failed:", err); - const error = err instanceof Error ? err : new Error(String(err)); - return { - status: "error", - message: error.message, - name: error.name, + const parsedError = err instanceof Error ? err : new Error(String(err)); + return { + status: "error", + message: parsedError.message, + name: parsedError.name, } as const; } }; diff --git a/apps/web/src/lib/auth-client.tsx b/apps/web/src/lib/auth-client.tsx index 5cdc7c52..1039a309 100644 --- a/apps/web/src/lib/auth-client.tsx +++ b/apps/web/src/lib/auth-client.tsx @@ -1,11 +1,11 @@ import { - useState, - useEffect, - useCallback, + createContext, + useCallback, useContext, + useEffect, useRef, - type ReactNode, + useState } from "react"; import { createAuthClient } from "better-auth/react"; import { @@ -14,6 +14,7 @@ import { } from "@convex-dev/better-auth/client/plugins"; import { env } from "./env"; import { analytics } from "./analytics"; +import type {ReactNode} from "react"; const AUTH_SESSION_COOKIE = "ba_session"; @@ -172,9 +173,7 @@ export function StableAuthProvider({ children }: { children: ReactNode }) { "User", image: result.data.user.image ?? null, }, - session: result.data.session - ? { id: result.data.session.id, token: result.data.session.token } - : null, + session: { id: result.data.session.id, token: result.data.session.token }, }); } else { setSessionData({ user: null, session: null }); diff --git a/apps/web/src/lib/env.ts b/apps/web/src/lib/env.ts index eb88c212..f6ca499a 100644 --- a/apps/web/src/lib/env.ts +++ b/apps/web/src/lib/env.ts @@ -7,8 +7,8 @@ export const env = { CONVEX_URL: import.meta.env.VITE_CONVEX_URL as string, CONVEX_SITE_URL: import.meta.env.VITE_CONVEX_SITE_URL as string, - POSTHOG_KEY: import.meta.env.VITE_POSTHOG_KEY as string | undefined, - POSTHOG_HOST: import.meta.env.VITE_POSTHOG_HOST as string | undefined, + POSTHOG_KEY: import.meta.env.VITE_POSTHOG_KEY, + POSTHOG_HOST: import.meta.env.VITE_POSTHOG_HOST, } as const; // Validate required env vars diff --git a/apps/web/src/lib/fuzzy-search.ts b/apps/web/src/lib/fuzzy-search.ts index 6838598a..00f9e1be 100644 --- a/apps/web/src/lib/fuzzy-search.ts +++ b/apps/web/src/lib/fuzzy-search.ts @@ -14,10 +14,10 @@ export function fuzzyMatch(text: string, query: string): boolean { } export function fuzzyFilter( - items: T[], + items: Array, query: string, - getSearchText: (item: T) => string | string[], -): T[] { + getSearchText: (item: T) => string | Array, +): Array { if (!query.trim()) return items; return items.filter((item) => { diff --git a/apps/web/src/lib/redis.ts b/apps/web/src/lib/redis.ts index 68d421b8..49755c3f 100644 --- a/apps/web/src/lib/redis.ts +++ b/apps/web/src/lib/redis.ts @@ -5,7 +5,8 @@ * Works with any Redis (self-hosted, Upstash, etc.) */ -import { createClient, type RedisClientType } from "redis"; +import { createClient } from "redis"; +import type {RedisClientType} from "redis"; let redisClient: RedisClientType | null = null; let isConnected = false; @@ -20,7 +21,7 @@ export function getRedisClient(): RedisClientType | null { if (!redisClient) { redisClient = createClient({ url: REDIS_URL }); - redisClient.on("error", (err) => { + redisClient.on("error", (err: Error) => { console.error("[Redis] Error:", err); isConnected = false; }); @@ -32,7 +33,7 @@ export function getRedisClient(): RedisClientType | null { console.log("[Redis] Disconnected"); isConnected = false; }); - connectionPromise = redisClient.connect().then(() => {}).catch((err) => { + connectionPromise = redisClient.connect().then(() => {}).catch((err: Error) => { console.error("[Redis] Connection failed:", err); isConnected = false; }); @@ -167,17 +168,17 @@ export async function errorStream(chatId: string, error: string): Promise export async function readStream( chatId: string, lastId: string = "0", -): Promise { +): Promise> { const client = await getConnectedClient(); if (!client) return []; const streamKey = keys.stream(chatId); const entries = await client.xRange(streamKey, lastId === "0" ? "-" : `(${lastId}`, "+"); - return entries.map((entry) => ({ + return entries.map((entry: { id: string; message: Record }) => ({ id: entry.id, text: entry.message.text || "", - type: (entry.message.type as StreamToken["type"]) || "text", + type: (entry.message.type as StreamToken["type"]), timestamp: parseInt(entry.message.ts || "0", 10), })); } @@ -213,17 +214,17 @@ export async function setTyping( } } -export async function getTypingUsers(chatId: string): Promise { +export async function getTypingUsers(chatId: string): Promise> { const client = await getConnectedClient(); if (!client) return []; const pattern = `chat:${chatId}:typing:*`; - const foundKeys: string[] = []; - for await (const keys of client.scanIterator({ MATCH: pattern, COUNT: 100 })) { - if (Array.isArray(keys)) { - foundKeys.push(...keys); + const foundKeys: Array = []; + for await (const scanKeys of client.scanIterator({ MATCH: pattern, COUNT: 100 })) { + if (Array.isArray(scanKeys)) { + foundKeys.push(...scanKeys); } else { - foundKeys.push(keys); + foundKeys.push(scanKeys); } } diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index a5ef1935..b7e7c0e9 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -1,6 +1,7 @@ -import { clsx, type ClassValue } from "clsx"; +import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; +import type {ClassValue} from "clsx"; -export function cn(...inputs: ClassValue[]) { +export function cn(...inputs: Array) { return twMerge(clsx(inputs)); } diff --git a/apps/web/src/providers/index.tsx b/apps/web/src/providers/index.tsx index d6597c7c..c1883abb 100644 --- a/apps/web/src/providers/index.tsx +++ b/apps/web/src/providers/index.tsx @@ -1,13 +1,13 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConvexProviderWithAuth, useMutation } from "convex/react"; import { Toaster } from "sonner"; +import { api } from "@server/convex/_generated/api"; import { convexClient } from "../lib/convex"; -import { authClient, useAuth, StableAuthProvider } from "../lib/auth-client"; +import { StableAuthProvider, authClient, useAuth } from "../lib/auth-client"; +import { prefetchModels } from "../stores/model"; import { ThemeProvider } from "./theme-provider"; import { PostHogProvider } from "./posthog"; -import { prefetchModels } from "../stores/model"; -import { api } from "@server/convex/_generated/api"; if (typeof window !== "undefined") { prefetchModels(); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 497bcb18..ee15f8e3 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -7,7 +7,7 @@ import { } from "@tanstack/react-router"; import { Providers } from "../providers"; import { CommandPalette, useCommandPaletteShortcut } from "../components/command-palette"; -import { SidebarProvider, SidebarInset, useSidebarShortcut } from "../components/ui/sidebar"; +import { SidebarInset, SidebarProvider, useSidebarShortcut } from "../components/ui/sidebar"; import { NavigationProgress } from "../components/navigation-progress"; import { AppSidebar } from "../components/app-sidebar"; import { useAuth } from "../lib/auth-client"; diff --git a/apps/web/src/routes/about.tsx b/apps/web/src/routes/about.tsx index 0f6c5c1b..e9ea11cc 100644 --- a/apps/web/src/routes/about.tsx +++ b/apps/web/src/routes/about.tsx @@ -2,7 +2,7 @@ * About Page - Information about osschat */ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { Link, createFileRoute } from "@tanstack/react-router"; import { Button } from "@/components/ui/button"; export const Route = createFileRoute("/about")({ diff --git a/apps/web/src/routes/api/chat.ts b/apps/web/src/routes/api/chat.ts index 5245aa52..bef01d63 100644 --- a/apps/web/src/routes/api/chat.ts +++ b/apps/web/src/routes/api/chat.ts @@ -1,17 +1,17 @@ import { createFileRoute } from "@tanstack/react-router"; import { json } from "@tanstack/react-start"; import { - streamText, convertToModelMessages, - stepCountIs, generateId, + stepCountIs, + streamText, } from "ai"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { webSearch } from "@valyu/ai-sdk"; -import { redis } from "@/lib/redis"; -import { convexServerClient } from "@/lib/convex-server"; import { api } from "@server/convex/_generated/api"; import type { Id } from "@server/convex/_generated/dataModel"; +import { redis } from "@/lib/redis"; +import { convexServerClient } from "@/lib/convex-server"; const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; const VALYU_API_KEY = process.env.VALYU_API_KEY; @@ -230,7 +230,7 @@ export const Route = createFileRoute("/api/chat")({ try { for await (const part of result.fullStream) { - if (abortSignal?.aborted) { + if (abortSignal.aborted) { console.log("[Chat API POST] Client disconnected, marking stream interrupted"); await markStreamInterrupted(); controller.close(); diff --git a/apps/web/src/routes/auth/sign-in.tsx b/apps/web/src/routes/auth/sign-in.tsx index 3461b203..b050d59f 100644 --- a/apps/web/src/routes/auth/sign-in.tsx +++ b/apps/web/src/routes/auth/sign-in.tsx @@ -2,8 +2,8 @@ * Sign In Page - GitHub OAuth authentication */ -import { useState, useEffect } from "react"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { Link, createFileRoute } from "@tanstack/react-router"; import { signInWithGitHub, signInWithVercel } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; diff --git a/apps/web/src/routes/c/$chatId.tsx b/apps/web/src/routes/c/$chatId.tsx index 156c2695..2fec15d9 100644 --- a/apps/web/src/routes/c/$chatId.tsx +++ b/apps/web/src/routes/c/$chatId.tsx @@ -6,7 +6,7 @@ * Uses OSSChat Cloud (free tier) by default, no API key required. */ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { Link, createFileRoute } from "@tanstack/react-router"; import { useAuth } from "@/lib/auth-client"; import { ChatInterface } from "@/components/chat-interface"; import { Button } from "@/components/ui/button"; diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 0a124974..e4c7a6ad 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -1,9 +1,9 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useAuth } from "../lib/auth-client"; import { Button } from "../components/ui/button"; import { ChatInterface } from "../components/chat-interface"; import { convexClient } from "../lib/convex"; -import { useRef, useCallback, useState, useEffect } from "react"; export const Route = createFileRoute('/')({ component: HomePage, @@ -12,7 +12,7 @@ export const Route = createFileRoute('/')({ const GAP = 2 function generateStaircaseSquares(gridSize: number) { - const squares: { col: number; row: number; key: string }[] = [] + const squares: Array<{ col: number; row: number; key: string }> = [] for (let row = 0; row < gridSize; row++) { const colsToFill = gridSize - row diff --git a/apps/web/src/routes/openrouter/callback.tsx b/apps/web/src/routes/openrouter/callback.tsx index 0d3ddec1..1101ad37 100644 --- a/apps/web/src/routes/openrouter/callback.tsx +++ b/apps/web/src/routes/openrouter/callback.tsx @@ -6,7 +6,7 @@ */ import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useOpenRouterKey } from "../../stores/openrouter"; import { Card, diff --git a/apps/web/src/routes/privacy.tsx b/apps/web/src/routes/privacy.tsx index 5da5f636..7d487d4f 100644 --- a/apps/web/src/routes/privacy.tsx +++ b/apps/web/src/routes/privacy.tsx @@ -2,7 +2,7 @@ * Privacy Policy Page */ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { Link, createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/privacy")({ head: () => ({ diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx index 30422ee0..f7be4d1c 100644 --- a/apps/web/src/routes/settings.tsx +++ b/apps/web/src/routes/settings.tsx @@ -2,23 +2,25 @@ * Settings Page */ -import { useState, type KeyboardEvent, type MouseEvent } from "react"; -import { createFileRoute, Link } from "@tanstack/react-router"; -import { useQuery, useMutation } from "convex/react"; +import { useState } from "react"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { useMutation, useQuery } from "convex/react"; import { api } from "@server/convex/_generated/api"; +import { CheckCircleIcon, CheckIcon, DatabaseIcon, Loader2Icon, PencilIcon, RefreshCwIcon, XIcon, ZapIcon } from "lucide-react"; +import type {KeyboardEvent, MouseEvent} from "react"; +import type {ChatTitleLength} from "@/stores/chat-title"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Separator } from "@/components/ui/separator"; -import { useAuth, signOut, authClient } from "@/lib/auth-client"; +import { authClient, signOut, useAuth } from "@/lib/auth-client"; import { useOpenRouterKey } from "@/stores/openrouter"; -import { useProviderStore, DAILY_LIMIT_CENTS } from "@/stores/provider"; -import { useModels, getCacheStatus } from "@/stores/model"; -import { useChatTitleStore, type ChatTitleLength } from "@/stores/chat-title"; +import { DAILY_LIMIT_CENTS, useProviderStore } from "@/stores/provider"; +import { getCacheStatus, useModels } from "@/stores/model"; +import { useChatTitleStore } from "@/stores/chat-title"; import { OpenRouterConnectModal } from "@/components/openrouter-connect-modal"; import { DeleteAccountModal } from "@/components/delete-account-modal"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; -import { RefreshCwIcon, DatabaseIcon, ZapIcon, CheckCircleIcon, PencilIcon, CheckIcon, XIcon, Loader2Icon } from "lucide-react"; import { Input } from "@/components/ui/input"; export const Route = createFileRoute("/settings")({ @@ -33,7 +35,7 @@ export const Route = createFileRoute("/settings")({ type Section = "account" | "providers" | "chat" | "models"; -const sections: { id: Section; label: string }[] = [ +const sections: Array<{ id: Section; label: string }> = [ { id: "account", label: "Account" }, { id: "providers", label: "Providers" }, { id: "chat", label: "Chat" }, @@ -573,7 +575,7 @@ function ProvidersSection() { ); } -const TITLE_LENGTH_OPTIONS: ChatTitleLength[] = ["short", "standard", "long"]; +const TITLE_LENGTH_OPTIONS: Array = ["short", "standard", "long"]; const TITLE_LENGTH_LABELS: Record = { short: "Concise (2-4 words)", standard: "Standard (4-6 words)", diff --git a/apps/web/src/routes/terms.tsx b/apps/web/src/routes/terms.tsx index 080d2ed2..d1a525a4 100644 --- a/apps/web/src/routes/terms.tsx +++ b/apps/web/src/routes/terms.tsx @@ -2,7 +2,7 @@ * Terms of Service Page */ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { Link, createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/terms")({ head: () => ({ diff --git a/apps/web/src/stores/model.ts b/apps/web/src/stores/model.ts index 39196f31..1963aa29 100644 --- a/apps/web/src/stores/model.ts +++ b/apps/web/src/stores/model.ts @@ -8,7 +8,7 @@ import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; -import { useEffect, useState, useMemo, useCallback } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { initPricingLookup } from "./provider"; import { analytics } from "@/lib/analytics"; @@ -32,7 +32,7 @@ interface OpenRouterModel { architecture?: { modality?: string; }; - supported_parameters?: string[]; + supported_parameters?: Array; } export interface Model { @@ -56,7 +56,7 @@ export interface Model { // Provider mapping for display names and logo IDs // ============================================================================ -const PROVIDER_INFO: Record = { +const PROVIDER_INFO: Partial> = { openai: { name: "OpenAI", logoId: "openai" }, anthropic: { name: "Anthropic", logoId: "anthropic" }, google: { name: "Google", logoId: "google" }, @@ -150,17 +150,17 @@ const PROVIDER_PRIORITY = [ // ============================================================================ interface ModelCache { - models: Model[] | null; + models: Array | null; timestamp: number; loading: boolean; error: Error | null; - promise: Promise | null; + promise: Promise> | null; } const CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours const STORAGE_KEY = "openchat-models-cache"; -function loadFromStorage(): { models: Model[] | null; timestamp: number } { +function loadFromStorage(): { models: Array | null; timestamp: number } { if (typeof window === "undefined") return { models: null, timestamp: 0 }; try { const stored = localStorage.getItem(STORAGE_KEY); @@ -176,7 +176,7 @@ function loadFromStorage(): { models: Model[] | null; timestamp: number } { return { models: null, timestamp: 0 }; } -function saveToStorage(models: Model[], timestamp: number) { +function saveToStorage(models: Array, timestamp: number) { if (typeof window === "undefined") return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ models, timestamp })); @@ -266,7 +266,7 @@ function extractFamily(id: string, name: string): string | undefined { } function transformModel(raw: OpenRouterModel): Model { - const id = raw.id as string; + const id = raw.id; const providerSlug = id.split("/")[0] || "unknown"; const info = PROVIDER_INFO[providerSlug] || { name: providerSlug.charAt(0).toUpperCase() + providerSlug.slice(1).replace(/-/g, " "), @@ -309,7 +309,7 @@ function transformModel(raw: OpenRouterModel): Model { // Fetch ALL models from OpenRouter // ============================================================================ -async function fetchAllModels(): Promise { +async function fetchAllModels(): Promise> { // Return cached if fresh if (cache.models && Date.now() - cache.timestamp < CACHE_TTL) { return cache.models; @@ -336,7 +336,7 @@ async function fetchAllModels(): Promise { const rawModels = data.data || []; // Transform all models - const models: Model[] = (rawModels as OpenRouterModel[]) + const models: Array = (rawModels as Array) .filter((m): m is OpenRouterModel & { id: string } => !!m.id && typeof m.id === "string") .map(transformModel) // Sort: Popular first, then by provider priority, then alphabetically @@ -396,7 +396,7 @@ export function clearModelCache() { } // Reload models (clear cache + fetch) -export async function reloadModels(): Promise { +export async function reloadModels(): Promise> { clearModelCache(); return fetchAllModels(); } @@ -405,7 +405,7 @@ export async function reloadModels(): Promise { // Fallback models // ============================================================================ -function getFallbackModels(): Model[] { +function getFallbackModels(): Array { return [ { id: "anthropic/claude-3.5-sonnet", @@ -458,7 +458,7 @@ function getFallbackModels(): Model[] { ]; } -export const models = getFallbackModels(); +export const defaultModels = getFallbackModels(); export const fallbackModels = getFallbackModels(); // ============================================================================ @@ -593,16 +593,16 @@ export const useModelStore = create()( merge: (persisted, current) => { const data = persisted as { selectedModelId?: string; - favorites?: string[]; + favorites?: Array; reasoningEffort?: ReasoningEffort; maxSteps?: number; }; return { ...current, - selectedModelId: data?.selectedModelId ?? current.selectedModelId, - favorites: new Set(data?.favorites ?? []), - reasoningEffort: data?.reasoningEffort ?? current.reasoningEffort, - maxSteps: data?.maxSteps ?? current.maxSteps, + selectedModelId: data.selectedModelId ?? current.selectedModelId, + favorites: new Set(data.favorites ?? []), + reasoningEffort: data.reasoningEffort ?? current.reasoningEffort, + maxSteps: data.maxSteps ?? current.maxSteps, }; }, }, @@ -616,7 +616,7 @@ export const useModelStore = create()( // ============================================================================ export function useModels() { - const [models, setModels] = useState(() => cache.models || getFallbackModels()); + const [models, setModels] = useState>(() => cache.models || getFallbackModels()); const [isLoading, setIsLoading] = useState(() => cache.loading || !cache.models); const [error, setError] = useState(() => cache.error); const [, forceUpdate] = useState(0); @@ -676,23 +676,19 @@ export function useModels() { } }, []); - // Group by provider const modelsByProvider = useMemo(() => { - const groups: Record = {}; + const groups: Record> = {}; for (const model of models) { - if (!groups[model.provider]) groups[model.provider] = []; - groups[model.provider].push(model); + (groups[model.provider] ??= []).push(model); } return groups; }, [models]); - // Group by family const modelsByFamily = useMemo(() => { - const groups: Record = {}; + const groups: Record> = {}; for (const model of models) { const key = model.family || model.provider; - if (!groups[key]) groups[key] = []; - groups[key].push(model); + (groups[key] ??= []).push(model); } return groups; }, [models]); @@ -743,8 +739,8 @@ export function useModels() { // Helpers // ============================================================================ -export function getModelById(models: Model[], id: string): Model | undefined { - return models.find((m) => m.id === id); +export function getModelById(modelList: Array, id: string): Model | undefined { + return modelList.find((m) => m.id === id); } export function prefetchModels() { diff --git a/apps/web/src/stores/openrouter.ts b/apps/web/src/stores/openrouter.ts index c48bdc20..7d48be88 100644 --- a/apps/web/src/stores/openrouter.ts +++ b/apps/web/src/stores/openrouter.ts @@ -6,13 +6,13 @@ */ import { create } from "zustand"; -import { persist, devtools } from "zustand/middleware"; +import { devtools, persist } from "zustand/middleware"; import { - initiateOAuthFlow, + clearOAuthStorage, + exchangeCodeForKey, getStoredCodeVerifier, + initiateOAuthFlow, validateState, - exchangeCodeForKey, - clearOAuthStorage, } from "../lib/openrouter-oauth"; // ============================================================================ diff --git a/apps/web/src/stores/pending-message.ts b/apps/web/src/stores/pending-message.ts index 6e6b9e62..741fbc8e 100644 --- a/apps/web/src/stores/pending-message.ts +++ b/apps/web/src/stores/pending-message.ts @@ -22,7 +22,7 @@ interface ChatFileAttachment { interface PendingMessage { chatId: string; text: string; - files?: ChatFileAttachment[]; + files?: Array; } interface PendingMessageStore { diff --git a/apps/web/src/stores/stream.ts b/apps/web/src/stores/stream.ts index 89529f71..bf7096c3 100644 --- a/apps/web/src/stores/stream.ts +++ b/apps/web/src/stores/stream.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { devtools, persist, createJSONStorage } from "zustand/middleware"; +import { createJSONStorage, devtools, persist } from "zustand/middleware"; export type StreamStatus = "idle" | "connecting" | "streaming" | "stopping" | "error" | "resuming"; diff --git a/apps/web/src/stores/ui.ts b/apps/web/src/stores/ui.ts index 62e80836..3bf74afa 100644 --- a/apps/web/src/stores/ui.ts +++ b/apps/web/src/stores/ui.ts @@ -3,7 +3,7 @@ */ import { create } from "zustand"; -import { persist, devtools } from "zustand/middleware"; +import { devtools, persist } from "zustand/middleware"; interface UIState { // Sidebar From f11f96dddae4809e78a7aa9e87778ae76468ead4 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 23 Jan 2026 18:12:56 -0500 Subject: [PATCH 2/2] fix: address AI review - restore SpeechRecognition fallback, isStreaming guard, and sidebar init --- apps/web/src/components/ai-elements/prompt-input.tsx | 2 +- apps/web/src/components/chat-interface.tsx | 1 + apps/web/src/components/ui/sidebar.tsx | 12 +++++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ai-elements/prompt-input.tsx b/apps/web/src/components/ai-elements/prompt-input.tsx index 706f5010..cdfc9c41 100644 --- a/apps/web/src/components/ai-elements/prompt-input.tsx +++ b/apps/web/src/components/ai-elements/prompt-input.tsx @@ -1119,7 +1119,7 @@ export const PromptInputSpeechButton = ({ typeof window !== "undefined" && ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) ) { - const SpeechRecognition = window.SpeechRecognition; + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const speechRecognition = new SpeechRecognition(); speechRecognition.continuous = true; diff --git a/apps/web/src/components/chat-interface.tsx b/apps/web/src/components/chat-interface.tsx index 0264d3b4..e3474e4f 100644 --- a/apps/web/src/components/chat-interface.tsx +++ b/apps/web/src/components/chat-interface.tsx @@ -365,6 +365,7 @@ function ChainOfThought({ hasAutoCollapsedRef.current = false; } else if ( wasStreamingRef.current && + !isStreaming && hasTextContent && !hasAutoCollapsedRef.current ) { diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 03639ad9..8447aa84 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -49,13 +49,19 @@ function SidebarProvider({ const [isMobile, setIsMobile] = React.useState(false); - // Initialize from store or defaults on first render + // Initialize from store or defaults React.useEffect(() => { - setSidebarOpen(defaultOpen); - setSidebarCollapsed(defaultCollapsed); + if (sidebarOpen === undefined) { + setSidebarOpen(defaultOpen); + } + if (sidebarCollapsed === undefined) { + setSidebarCollapsed(defaultCollapsed); + } }, [ defaultOpen, defaultCollapsed, + sidebarOpen, + sidebarCollapsed, setSidebarOpen, setSidebarCollapsed, ]);