fix(chat): preserve compare state and align composer controls
This commit is contained in:
parent
def605d5b6
commit
7bc44536ea
6 changed files with 170 additions and 71 deletions
|
|
@ -644,7 +644,7 @@ export function AppSidebar() {
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className="app-user-menu menu-soft-surface menu-flat-destructive ring-0 w-44 py-2 font-heading rounded-[14px] border-0"
|
||||
>
|
||||
|
|
@ -754,7 +754,7 @@ export function AppSidebar() {
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className="app-user-menu menu-soft-surface menu-flat-destructive ring-0 w-44 py-2 font-heading rounded-[14px] border-0"
|
||||
>
|
||||
|
|
@ -835,7 +835,7 @@ export function AppSidebar() {
|
|||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="app-user-menu menu-soft-surface-up ring-0 w-[15rem] py-2.5 font-heading rounded-[14px] border-0"
|
||||
className="app-user-menu menu-soft-surface-up ring-0 py-2.5 font-heading rounded-[14px] border-0"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@ export const Thread: FC<{
|
|||
hideComposer?: boolean;
|
||||
hideWelcome?: boolean;
|
||||
targetThreadId?: string;
|
||||
}> = ({ hideComposer, hideWelcome, targetThreadId }) => {
|
||||
onEnterCompare?: () => void;
|
||||
}> = ({ hideComposer, hideWelcome, targetThreadId, onEnterCompare }) => {
|
||||
// Intent-aware autoscroll: replaces assistant-ui's built-in autoscroll
|
||||
// to prevent the streaming-mutation race that makes the viewport snap
|
||||
// back to the bottom while the user is scrolling up (see the hook for
|
||||
|
|
@ -171,6 +172,7 @@ export const Thread: FC<{
|
|||
<ThreadWelcome
|
||||
hideComposer={hideComposer}
|
||||
threadId={threadId}
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
</AuiIf>
|
||||
)}
|
||||
|
|
@ -214,6 +216,7 @@ export const Thread: FC<{
|
|||
<ThreadComposerDock
|
||||
disabled={isComposerAttachPending}
|
||||
threadId={threadId}
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
</AuiIf>
|
||||
)}
|
||||
|
|
@ -318,7 +321,8 @@ const GeneratedImageViewportOverlay: FC<{ hideComposer?: boolean }> = ({
|
|||
const ThreadComposerDock: FC<{
|
||||
disabled?: boolean;
|
||||
threadId?: string | null;
|
||||
}> = ({ disabled, threadId }) => {
|
||||
onEnterCompare?: () => void;
|
||||
}> = ({ disabled, threadId, onEnterCompare }) => {
|
||||
const { overlay } = useGeneratedImageOverlay();
|
||||
|
||||
return (
|
||||
|
|
@ -338,6 +342,7 @@ const ThreadComposerDock: FC<{
|
|||
disabled={disabled}
|
||||
threadId={threadId}
|
||||
menuSide="top"
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
</div>
|
||||
<p className="composer-footer-note">
|
||||
|
|
@ -383,7 +388,8 @@ function getWelcomeEmoji(): string {
|
|||
const ThreadWelcome: FC<{
|
||||
hideComposer?: boolean;
|
||||
threadId?: string | null;
|
||||
}> = ({ hideComposer, threadId }) => {
|
||||
onEnterCompare?: () => void;
|
||||
}> = ({ hideComposer, threadId, onEnterCompare }) => {
|
||||
const [currentEmoji] = useState(getWelcomeEmoji);
|
||||
|
||||
const currentEmojiSrc =
|
||||
|
|
@ -401,7 +407,12 @@ const ThreadWelcome: FC<{
|
|||
What’s on your mind today?
|
||||
</h1>
|
||||
</div>
|
||||
{!hideComposer && <ComposerAnimated threadId={threadId} />}
|
||||
{!hideComposer && (
|
||||
<ComposerAnimated
|
||||
threadId={threadId}
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -412,11 +423,17 @@ const ComposerAnimated: FC<{
|
|||
disabled?: boolean;
|
||||
threadId?: string | null;
|
||||
menuSide?: "top" | "bottom";
|
||||
}> = ({ disabled, threadId, menuSide }) => {
|
||||
onEnterCompare?: () => void;
|
||||
}> = ({ disabled, threadId, menuSide, onEnterCompare }) => {
|
||||
return (
|
||||
<div className="relative mx-auto min-w-0 w-full max-w-[660px]">
|
||||
<div className="relative z-10 w-full">
|
||||
<Composer disabled={disabled} threadId={threadId} menuSide={menuSide} />
|
||||
<Composer
|
||||
disabled={disabled}
|
||||
threadId={threadId}
|
||||
menuSide={menuSide}
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -450,7 +467,8 @@ const Composer: FC<{
|
|||
disabled?: boolean;
|
||||
threadId?: string | null;
|
||||
menuSide?: "top" | "bottom";
|
||||
}> = ({ disabled, threadId, menuSide }) => {
|
||||
onEnterCompare?: () => void;
|
||||
}> = ({ disabled, threadId, menuSide, onEnterCompare }) => {
|
||||
const aui = useAui();
|
||||
const { overlay, closeOverlay } = useGeneratedImageOverlay();
|
||||
const setImageToolsEnabled = useChatRuntimeStore(
|
||||
|
|
@ -634,7 +652,10 @@ const Composer: FC<{
|
|||
data-expanded={composerExpanded ? "true" : "false"}
|
||||
>
|
||||
<div className="unsloth-composer-left">
|
||||
<ComposerToolsMenu side={effectiveMenuSide} />
|
||||
<ComposerToolsMenu
|
||||
side={effectiveMenuSide}
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
{composerExpanded ? (
|
||||
<>
|
||||
{toolsEnabled ? <WebSearchToggle /> : null}
|
||||
|
|
@ -969,6 +990,7 @@ const ReasoningToggle: FC<{ side?: "top" | "bottom" }> = ({
|
|||
{
|
||||
isReasoningProvider:
|
||||
selectedExternalProvider?.isReasoningModel === true,
|
||||
baseUrl: selectedExternalProvider?.baseUrl ?? null,
|
||||
},
|
||||
)
|
||||
: null;
|
||||
|
|
@ -1453,9 +1475,10 @@ const PROJECTS_PR_URL = "https://github.com/unslothai/unsloth/pull/5725";
|
|||
|
||||
// Plus menu: attachment and workflow actions. Opens downward in the centered
|
||||
// welcome composer; the docked composer passes side="top" to open upward.
|
||||
const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
||||
side = "bottom",
|
||||
}) => {
|
||||
const ComposerToolsMenu: FC<{
|
||||
side?: "top" | "bottom";
|
||||
onEnterCompare?: () => void;
|
||||
}> = ({ side = "bottom", onEnterCompare }) => {
|
||||
const navigate = useNavigate();
|
||||
const setSettingsPanelOpen = useChatRuntimeStore(
|
||||
(s) => s.setSettingsPanelOpen,
|
||||
|
|
@ -1536,11 +1559,15 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
|||
}, [codeMenuDisabled, codeToolsEnabled, setCodeToolsEnabled]);
|
||||
|
||||
const startCompare = useCallback(() => {
|
||||
if (onEnterCompare) {
|
||||
onEnterCompare();
|
||||
return;
|
||||
}
|
||||
const store = useChatRuntimeStore.getState();
|
||||
store.setActiveThreadId(null);
|
||||
store.setContextUsage(null);
|
||||
navigate({ to: "/chat", search: { compare: createCompareId() } });
|
||||
}, [navigate]);
|
||||
}, [navigate, onEnterCompare]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
|
@ -1641,17 +1668,17 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
|||
More
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="unsloth-plus-menu w-[200px]">
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={true}>
|
||||
<HugeiconsIcon icon={PencilRulerIcon} strokeWidth={2} />
|
||||
Canvas
|
||||
Canvas (coming soon)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => startCompare()}>
|
||||
<Columns2Icon />
|
||||
Compare chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={true}>
|
||||
<HugeiconsIcon icon={DatabaseIcon} strokeWidth={2} />
|
||||
RAG
|
||||
RAG (coming soon)
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,12 @@ function messageHasImage(message: MessageRecord): boolean {
|
|||
const SingleContent = memo(function SingleContent({
|
||||
threadId,
|
||||
newThreadNonce,
|
||||
}: { threadId?: string; newThreadNonce?: string }): ReactElement {
|
||||
onEnterCompare,
|
||||
}: {
|
||||
threadId?: string;
|
||||
newThreadNonce?: string;
|
||||
onEnterCompare?: () => void;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<ChatRuntimeProvider
|
||||
modelType="base"
|
||||
|
|
@ -167,7 +172,11 @@ const SingleContent = memo(function SingleContent({
|
|||
newThreadNonce={newThreadNonce}
|
||||
>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 basis-0 flex-col overflow-hidden">
|
||||
<Thread hideWelcome={Boolean(threadId)} targetThreadId={threadId} />
|
||||
<Thread
|
||||
hideWelcome={Boolean(threadId)}
|
||||
targetThreadId={threadId}
|
||||
onEnterCompare={onEnterCompare}
|
||||
/>
|
||||
</div>
|
||||
</ChatRuntimeProvider>
|
||||
);
|
||||
|
|
@ -1204,7 +1213,14 @@ export function ChatPage(): ReactElement {
|
|||
const openSidebar = useCallback(() => setPinned(true), [setPinned]);
|
||||
|
||||
const enterCompare = useCallback(() => {
|
||||
setViewBeforeCompare({ ...search });
|
||||
const saved: ChatSearch = { ...search };
|
||||
const active = useChatRuntimeStore.getState().activeThreadId;
|
||||
if (!(saved.thread || saved.new) && active) {
|
||||
if (!active.startsWith("__LOCALID_")) {
|
||||
saved.thread = active;
|
||||
}
|
||||
}
|
||||
setViewBeforeCompare(saved);
|
||||
useChatRuntimeStore.getState().setActiveThreadId(null);
|
||||
useChatRuntimeStore.getState().setContextUsage(null);
|
||||
navigate({ to: "/chat", search: { compare: createCompareId() } });
|
||||
|
|
@ -1618,6 +1634,7 @@ export function ChatPage(): ReactElement {
|
|||
key={view.threadId ?? "single"}
|
||||
threadId={view.threadId}
|
||||
newThreadNonce={view.newThreadNonce}
|
||||
onEnterCompare={enterCompare}
|
||||
/>
|
||||
) : (
|
||||
<CompareContent
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { CodeToggleIcon } from "@/components/assistant-ui/code-toggle-icon";
|
||||
import {
|
||||
thinkEffortAriaLabel,
|
||||
thinkToggleAriaLabel,
|
||||
} from "@/components/assistant-ui/think-aria-label";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { openLink } from "@/lib/open-link";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -22,11 +20,23 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { applyQwenThinkingParams } from "@/features/chat";
|
||||
import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils";
|
||||
import { isTauri } from "@/lib/api-base";
|
||||
import { isMultimodalResponse } from "./types/api";
|
||||
import { getImageInputUnavailableReason } from "./utils/image-input-support";
|
||||
import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils";
|
||||
import { openLink } from "@/lib/open-link";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAui } from "@assistant-ui/react";
|
||||
import {
|
||||
AttachmentIcon,
|
||||
CodeIcon,
|
||||
DatabaseIcon,
|
||||
File02Icon,
|
||||
Folder01Icon,
|
||||
Image03Icon,
|
||||
PencilRulerIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
ArrowUpIcon,
|
||||
CheckIcon,
|
||||
|
|
@ -42,34 +52,6 @@ import {
|
|||
SquareIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AttachmentIcon,
|
||||
CodeIcon,
|
||||
DatabaseIcon,
|
||||
File02Icon,
|
||||
Folder01Icon,
|
||||
Image03Icon,
|
||||
PencilRulerIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { loadModel, validateModel } from "./api/chat-api";
|
||||
import {
|
||||
parseExternalModelId,
|
||||
providerTypeSupportsVision,
|
||||
} from "./external-providers";
|
||||
import { useExternalProvidersStore } from "./stores/external-providers-store";
|
||||
import {
|
||||
type ReasoningEffort,
|
||||
useChatRuntimeStore,
|
||||
} from "./stores/chat-runtime-store";
|
||||
import {
|
||||
getExternalReasoningCapabilities,
|
||||
providerSupportsBuiltinCodeExecution,
|
||||
providerSupportsBuiltinImageGeneration,
|
||||
providerSupportsBuiltinWebFetch,
|
||||
} from "./provider-capabilities";
|
||||
import {
|
||||
type CompositionEvent,
|
||||
type FC,
|
||||
|
|
@ -85,6 +67,25 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { loadModel, validateModel } from "./api/chat-api";
|
||||
import {
|
||||
parseExternalModelId,
|
||||
providerTypeSupportsVision,
|
||||
} from "./external-providers";
|
||||
import {
|
||||
getExternalReasoningCapabilities,
|
||||
providerSupportsBuiltinCodeExecution,
|
||||
providerSupportsBuiltinImageGeneration,
|
||||
providerSupportsBuiltinWebFetch,
|
||||
} from "./provider-capabilities";
|
||||
import {
|
||||
type ReasoningEffort,
|
||||
useChatRuntimeStore,
|
||||
} from "./stores/chat-runtime-store";
|
||||
import { useExternalProvidersStore } from "./stores/external-providers-store";
|
||||
import { isMultimodalResponse } from "./types/api";
|
||||
import { createSafeId } from "./utils/compare-id";
|
||||
import { getImageInputUnavailableReason } from "./utils/image-input-support";
|
||||
|
||||
// Projects is still in development; its menu entries link to the tracking PR.
|
||||
const PROJECTS_PR_URL = "https://github.com/unslothai/unsloth/pull/5725";
|
||||
|
|
@ -342,6 +343,12 @@ export function RegisterCompareHandle({
|
|||
}
|
||||
|
||||
type PendingImage = { id: string; file: File };
|
||||
type PendingAudio = { name: string; base64: string };
|
||||
type ComposerDraft = {
|
||||
text: string;
|
||||
pendingImages: PendingImage[];
|
||||
pendingAudio: PendingAudio | null;
|
||||
};
|
||||
|
||||
function PendingImageThumb({
|
||||
file,
|
||||
|
|
@ -403,10 +410,7 @@ export function SharedComposer({
|
|||
const [running, setRunning] = useState(false);
|
||||
const [comparing, setComparing] = useState(false);
|
||||
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
|
||||
const [pendingAudio, setPendingAudio] = useState<{
|
||||
name: string;
|
||||
base64: string;
|
||||
} | null>(null);
|
||||
const [pendingAudio, setPendingAudio] = useState<PendingAudio | null>(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
|
@ -414,6 +418,15 @@ export function SharedComposer({
|
|||
const stuckImeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const audioInputRef = useRef<HTMLInputElement>(null);
|
||||
const composerDraftRef = useRef<ComposerDraft>({
|
||||
text: "",
|
||||
pendingImages: [],
|
||||
pendingAudio: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
composerDraftRef.current = { text, pendingImages, pendingAudio };
|
||||
}, [text, pendingImages, pendingAudio]);
|
||||
|
||||
const activeModel = useChatRuntimeStore((s) => {
|
||||
const checkpoint = s.params.checkpoint;
|
||||
|
|
@ -702,7 +715,7 @@ export function SharedComposer({
|
|||
droppedImageForUnavailable = true;
|
||||
continue;
|
||||
}
|
||||
next.push({ id: crypto.randomUUID(), file });
|
||||
next.push({ id: createSafeId(), file });
|
||||
}
|
||||
if (droppedImageForUnavailable && attachUnavailableReason) {
|
||||
toast.error(attachUnavailableReason);
|
||||
|
|
@ -785,6 +798,38 @@ export function SharedComposer({
|
|||
return;
|
||||
}
|
||||
|
||||
const draft: ComposerDraft = { text, pendingImages, pendingAudio };
|
||||
const clearComposerDraft = () => {
|
||||
composerDraftRef.current = {
|
||||
text: "",
|
||||
pendingImages: [],
|
||||
pendingAudio: null,
|
||||
};
|
||||
setText("");
|
||||
setPendingImages([]);
|
||||
setPendingAudio(null);
|
||||
clearPendingAudioStore();
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
const restoreComposerDraft = () => {
|
||||
const current = composerDraftRef.current;
|
||||
const hasNewContent =
|
||||
current.text.trim().length > 0 ||
|
||||
current.pendingImages.length > 0 ||
|
||||
current.pendingAudio !== null;
|
||||
if (hasNewContent) return;
|
||||
composerDraftRef.current = draft;
|
||||
setText(draft.text);
|
||||
setPendingImages(draft.pendingImages);
|
||||
setPendingAudio(draft.pendingAudio);
|
||||
if (draft.pendingAudio) {
|
||||
setPendingAudioStore(
|
||||
draft.pendingAudio.base64,
|
||||
draft.pendingAudio.name,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const content: CompareMessagePart[] = [];
|
||||
for (const { file } of pendingImages) {
|
||||
try {
|
||||
|
|
@ -802,11 +847,7 @@ export function SharedComposer({
|
|||
}
|
||||
if (content.length === 0) return;
|
||||
|
||||
setText("");
|
||||
setPendingImages([]);
|
||||
setPendingAudio(null);
|
||||
clearPendingAudioStore();
|
||||
textareaRef.current?.focus();
|
||||
clearComposerDraft();
|
||||
|
||||
// Generalized compare: load each model before dispatching to its side
|
||||
if (isGeneralizedCompare) {
|
||||
|
|
@ -960,6 +1001,7 @@ export function SharedComposer({
|
|||
|
||||
toast.success("Compare complete", { id: toastId, duration: 2000 });
|
||||
} catch (err) {
|
||||
restoreComposerDraft();
|
||||
toast.error("Compare failed", {
|
||||
id: toastId,
|
||||
description: err instanceof Error ? err.message : "Unknown error",
|
||||
|
|
@ -1225,9 +1267,9 @@ export function SharedComposer({
|
|||
More
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="unsloth-plus-menu w-[200px]">
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={true}>
|
||||
<HugeiconsIcon icon={PencilRulerIcon} strokeWidth={2} />
|
||||
Canvas
|
||||
Canvas (coming soon)
|
||||
</DropdownMenuItem>
|
||||
{/* Always active: this menu only renders in compare mode.
|
||||
Ticked like Web search/Code; click toggles it off. */}
|
||||
|
|
@ -1239,9 +1281,9 @@ export function SharedComposer({
|
|||
Compare chat
|
||||
<CheckIcon className="ml-auto" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={true}>
|
||||
<HugeiconsIcon icon={DatabaseIcon} strokeWidth={2} />
|
||||
RAG
|
||||
RAG (coming soon)
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
export function createCompareId(): string {
|
||||
export function createSafeId(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
export function createCompareId(): string {
|
||||
return createSafeId();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -859,6 +859,15 @@
|
|||
--menu-soft-shadow: rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.app-user-menu.menu-soft-surface,
|
||||
.app-user-menu.menu-soft-surface-up {
|
||||
box-shadow: 0 2px 8px -2px rgba(27, 27, 31, 0.16);
|
||||
}
|
||||
.dark .app-user-menu.menu-soft-surface,
|
||||
.dark .app-user-menu.menu-soft-surface-up {
|
||||
box-shadow: 0 3px 12px -6px rgba(0, 0, 0, 0.46);
|
||||
}
|
||||
|
||||
.chat-composer-surface {
|
||||
@apply relative flex w-full flex-col rounded-[32px] bg-white dark:bg-card px-3 py-3 outline-none transition-shadow;
|
||||
font-family: var(--font-sans);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue