mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-09 03:18:04 +00:00
fix chat selector ui and edit state
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
320568a388
commit
f93b1f906f
4 changed files with 134 additions and 66 deletions
|
|
@ -165,6 +165,10 @@ export function ChatSidebar({
|
|||
const pendingRegenerationRef = useRef<{
|
||||
text: string
|
||||
} | null>(null)
|
||||
const pendingSendSettingsRef = useRef<{
|
||||
model: ModelId
|
||||
reasoningEffort: ReasoningEffort
|
||||
} | null>(null)
|
||||
const [regenerationBaseLength, setRegenerationBaseLength] = useState<
|
||||
number | null
|
||||
>(null)
|
||||
|
|
@ -246,24 +250,30 @@ export function ChatSidebar({
|
|||
new DefaultChatTransport({
|
||||
api: `${chatApiBase}/chat`,
|
||||
credentials: "include",
|
||||
prepareSendMessagesRequest: ({ messages }) => ({
|
||||
body: {
|
||||
messages,
|
||||
metadata: {
|
||||
chatId: chatIdRef.current,
|
||||
projectId: selectedProjectRef.current,
|
||||
spaceMode:
|
||||
selectedProjectRef.current === AUTO_CHAT_SPACE_ID
|
||||
? "auto"
|
||||
: "manual",
|
||||
enableSpaceDiscovery:
|
||||
selectedProjectRef.current === AUTO_CHAT_SPACE_ID,
|
||||
model: selectedModelRef.current,
|
||||
reasoningEffort: reasoningEffortRef.current,
|
||||
truncateFromMessageId: truncateFromMessageIdRef.current,
|
||||
prepareSendMessagesRequest: ({ messages }) => {
|
||||
const sendSettings = pendingSendSettingsRef.current
|
||||
pendingSendSettingsRef.current = null
|
||||
|
||||
return {
|
||||
body: {
|
||||
messages,
|
||||
metadata: {
|
||||
chatId: chatIdRef.current,
|
||||
projectId: selectedProjectRef.current,
|
||||
spaceMode:
|
||||
selectedProjectRef.current === AUTO_CHAT_SPACE_ID
|
||||
? "auto"
|
||||
: "manual",
|
||||
enableSpaceDiscovery:
|
||||
selectedProjectRef.current === AUTO_CHAT_SPACE_ID,
|
||||
model: sendSettings?.model ?? selectedModelRef.current,
|
||||
reasoningEffort:
|
||||
sendSettings?.reasoningEffort ?? reasoningEffortRef.current,
|
||||
truncateFromMessageId: truncateFromMessageIdRef.current,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
}),
|
||||
[chatApiBase],
|
||||
)
|
||||
|
|
@ -405,10 +415,10 @@ export function ChatSidebar({
|
|||
if (messageIndex === -1) return
|
||||
|
||||
truncateFromMessageIdRef.current = messageId
|
||||
selectedModelRef.current = model
|
||||
reasoningEffortRef.current = nextReasoningEffort
|
||||
setSelectedModel(model)
|
||||
setReasoningEffort(nextReasoningEffort)
|
||||
pendingSendSettingsRef.current = {
|
||||
model,
|
||||
reasoningEffort: nextReasoningEffort,
|
||||
}
|
||||
clearError()
|
||||
pendingRegenerationRef.current = {
|
||||
text: trimmed,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export const UserMessage = memo(function UserMessage({
|
|||
selectedModel={editModel}
|
||||
onModelChange={handleEditModelChange}
|
||||
minimal
|
||||
dropdownDirection="down"
|
||||
/>
|
||||
<ReasoningSelector
|
||||
value={editReasoningEffort}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"
|
|||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { CheckIcon, ChevronDownIcon } from "lucide-react"
|
||||
import { models, type ModelId, modelNames } from "@/lib/models"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
|
|
@ -13,12 +13,14 @@ interface ChatModelSelectorProps {
|
|||
onModelChange?: (model: ModelId) => void
|
||||
/** Compact pill matching inline send control. */
|
||||
minimal?: boolean
|
||||
dropdownDirection?: "up" | "down"
|
||||
}
|
||||
|
||||
export default function ChatModelSelector({
|
||||
selectedModel: selectedModelProp,
|
||||
onModelChange,
|
||||
minimal = false,
|
||||
dropdownDirection = "up",
|
||||
}: ChatModelSelectorProps = {}) {
|
||||
const [internalModel, setInternalModel] =
|
||||
useState<ModelId>("claude-sonnet-4.6")
|
||||
|
|
@ -41,6 +43,7 @@ export default function ChatModelSelector({
|
|||
|
||||
const selectedModel = selectedModelProp ?? internalModel
|
||||
const currentModelData = modelNames[selectedModel]
|
||||
const selectedModelLabel = `${currentModelData.name} ${currentModelData.version}`
|
||||
|
||||
const handleModelSelect = (modelId: ModelId) => {
|
||||
if (onModelChange) {
|
||||
|
|
@ -56,71 +59,102 @@ export default function ChatModelSelector({
|
|||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex max-w-[min(100%,220px)] min-w-0 shrink cursor-pointer items-center gap-1.5 rounded-full bg-fg-primary/5 px-3 py-1.5 text-sm transition-colors hover:bg-fg-primary/10",
|
||||
"flex max-w-[min(100%,220px)] min-w-0 shrink cursor-pointer items-center gap-1.5 rounded-full border border-white/15 bg-black px-3 py-1.5 text-sm text-white transition-colors hover:border-white/30 hover:bg-white/5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-expanded={isOpen}
|
||||
aria-label={`Model: ${selectedModelLabel}`}
|
||||
>
|
||||
<p className="min-w-0 truncate text-left text-fg-primary">
|
||||
<p className="min-w-0 truncate text-left text-white">
|
||||
{currentModelData.name}{" "}
|
||||
<span className="text-fg-subtle">{currentModelData.version}</span>
|
||||
<span className="text-white/55">{currentModelData.version}</span>
|
||||
</p>
|
||||
<ChevronDownIcon className="size-3.5 shrink-0 text-fg-subtle" />
|
||||
<ChevronDownIcon className="size-3.5 shrink-0 text-white/55" />
|
||||
</button>
|
||||
) : (
|
||||
<Button
|
||||
variant="headers"
|
||||
className={cn(
|
||||
"h-10! max-w-[min(100%,220px)] shrink gap-1 rounded-full border-[#73737333] bg-surface-base text-base",
|
||||
"h-10! max-w-[min(100%,220px)] shrink gap-1.5 rounded-full border-white/15 bg-black text-base text-white shadow-none transition-colors hover:border-white/30 hover:bg-white/5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-expanded={isOpen}
|
||||
aria-label={`Model: ${selectedModelLabel}`}
|
||||
>
|
||||
<p className="truncate text-sm">
|
||||
{currentModelData.name}{" "}
|
||||
<span className="text-[#737373]">{currentModelData.version}</span>
|
||||
<span className="text-white/55">{currentModelData.version}</span>
|
||||
</p>
|
||||
<ChevronDownIcon className="size-4 text-[#737373]" />
|
||||
<ChevronDownIcon className="size-4 text-white/55" />
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative z-10 flex min-w-0 shrink items-center gap-2"
|
||||
className={cn(
|
||||
"relative flex min-w-0 shrink items-center gap-2",
|
||||
isOpen ? "z-[100]" : "z-10",
|
||||
)}
|
||||
>
|
||||
{trigger}
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute bottom-full left-0 mb-2 w-64 bg-surface-card backdrop-blur-xl border border-surface-border rounded-lg shadow-xl z-50 overflow-hidden">
|
||||
<div className="p-2 space-y-1">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 z-[100] w-[min(18rem,calc(100vw-2rem))] overflow-hidden rounded-xl border border-white/15 bg-black p-1 shadow-[0_18px_48px_rgba(0,0,0,0.55)]",
|
||||
dropdownDirection === "up" ? "bottom-full mb-2" : "top-full mt-2",
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{models.map((model) => {
|
||||
const modelData = modelNames[model.id]
|
||||
const isSelected = selectedModel === model.id
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex flex-col items-start p-2 px-3 rounded-md transition-colors cursor-pointer w-full text-left",
|
||||
selectedModel === model.id
|
||||
? "bg-[#293952]/60"
|
||||
: "hover:bg-[#293952]/40",
|
||||
"flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors",
|
||||
isSelected
|
||||
? "bg-[#E6E6E6] text-[#101010]"
|
||||
: "text-white hover:bg-white/10",
|
||||
)}
|
||||
onClick={() => handleModelSelect(model.id)}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" && handleModelSelect(model.id)
|
||||
}
|
||||
>
|
||||
<div className="text-sm font-medium text-white">
|
||||
{modelData.name}{" "}
|
||||
<span className="text-fg-subtle">{modelData.version}</span>
|
||||
</div>
|
||||
<div className="text-xs text-fg-muted truncate w-full">
|
||||
{model.description}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className={cn(
|
||||
"truncate text-[15px] font-medium",
|
||||
isSelected ? "text-[#101010]" : "text-white",
|
||||
)}
|
||||
>
|
||||
{modelData.name}{" "}
|
||||
<span
|
||||
className={cn(
|
||||
isSelected ? "text-[#101010]/60" : "text-white/55",
|
||||
)}
|
||||
>
|
||||
{modelData.version}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 truncate text-xs",
|
||||
isSelected ? "text-[#101010]/60" : "text-white/45",
|
||||
)}
|
||||
>
|
||||
{model.description}
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckIcon className="size-4 shrink-0 text-[#101010]/70" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { BrainIcon, CheckIcon, MoreHorizontalIcon, ZapIcon } from "lucide-react"
|
||||
import {
|
||||
BrainIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
MoreHorizontalIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { reasoningOptions, type ReasoningEffort } from "@/lib/models"
|
||||
|
|
@ -25,6 +31,7 @@ export function ReasoningSelector({
|
|||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const selected = reasoningOptions.find((option) => option.id === value)
|
||||
const SelectedIcon = value === "thinking" ? BrainIcon : ZapIcon
|
||||
const selectedLabel = selected?.label ?? "Reasoning"
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
|
@ -48,7 +55,10 @@ export function ReasoningSelector({
|
|||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative z-10 flex shrink-0 items-center"
|
||||
className={cn(
|
||||
"relative flex shrink-0 items-center",
|
||||
isOpen ? "z-[100]" : "z-10",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -58,18 +68,22 @@ export function ReasoningSelector({
|
|||
"cursor-pointer transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variant === "icon"
|
||||
? "rounded p-1.5 hover:bg-white/10"
|
||||
: "flex items-center gap-1 rounded-full bg-fg-primary/5 px-2.5 py-1 text-[13px] hover:bg-fg-primary/10",
|
||||
: "flex size-9 items-center justify-center gap-1.5 rounded-full border border-white/15 bg-black px-0 py-1.5 text-[13px] text-white hover:border-white/30 hover:bg-white/5 sm:size-auto sm:justify-start sm:px-2.5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
title="Reasoning"
|
||||
aria-label="Reasoning"
|
||||
title={`Reasoning: ${selectedLabel}`}
|
||||
aria-label={`Reasoning: ${selectedLabel}`}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{variant === "icon" ? (
|
||||
<MoreHorizontalIcon className="size-3.5 text-white/50 hover:text-white/80" />
|
||||
) : (
|
||||
<>
|
||||
<SelectedIcon className="size-3 shrink-0 text-fg-subtle" />
|
||||
<span className="text-fg-primary">{selected?.label}</span>
|
||||
<SelectedIcon className="size-3.5 shrink-0 text-white/65" />
|
||||
<span className="hidden text-white sm:inline">
|
||||
{selected?.label}
|
||||
</span>
|
||||
<ChevronDownIcon className="hidden size-3.5 shrink-0 text-white/55 sm:block" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -77,16 +91,11 @@ export function ReasoningSelector({
|
|||
{isOpen && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 z-50 w-44 overflow-hidden rounded-lg border border-surface-border bg-surface-card shadow-xl backdrop-blur-xl",
|
||||
"absolute left-0 z-[100] w-[min(14rem,calc(100vw-2rem))] overflow-hidden rounded-xl border border-white/15 bg-black p-1 shadow-[0_18px_48px_rgba(0,0,0,0.55)]",
|
||||
dropdownDirection === "up" ? "bottom-full mb-2" : "top-full mt-2",
|
||||
)}
|
||||
>
|
||||
<div className="border-b border-surface-border px-3 py-1.5">
|
||||
<span className="text-[11px] font-medium text-fg-muted">
|
||||
Reasoning effort
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<div className="space-y-1">
|
||||
{reasoningOptions.map((option) => {
|
||||
const Icon = option.id === "thinking" ? BrainIcon : ZapIcon
|
||||
const isSelected = option.id === value
|
||||
|
|
@ -96,16 +105,30 @@ export function ReasoningSelector({
|
|||
type="button"
|
||||
onClick={() => handleSelect(option.id)}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-left transition-colors",
|
||||
isSelected ? "bg-[#293952]/60" : "hover:bg-[#293952]/40",
|
||||
"flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-3 py-2.5 text-left transition-colors",
|
||||
isSelected
|
||||
? "bg-[#E6E6E6] text-[#101010]"
|
||||
: "text-white hover:bg-white/10",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3 shrink-0 text-white/60" />
|
||||
<span className="flex-1 text-[13px] font-medium text-white">
|
||||
{option.label}
|
||||
</span>
|
||||
<Icon
|
||||
className={cn(
|
||||
"size-4 shrink-0",
|
||||
isSelected ? "text-[#101010]/70" : "text-white/65",
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className={cn(
|
||||
"text-[15px] font-medium",
|
||||
isSelected ? "text-[#101010]" : "text-white",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckIcon className="size-3 shrink-0 text-[#E052A0]" />
|
||||
<CheckIcon className="size-4 shrink-0 text-[#101010]/70" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue