refactor(ai-chat): split ai message or user message component

- Introduced UserChatMessage and UserMessageParts components to handle user messages distinctly, allowing for better editing and interaction.
- Updated AIChatMessage to streamline message rendering and improve markdown formatting.
- Enhanced ToolInvocationComponent with accordion functionality for better organization of tool call details.
- Adjusted layout properties across various components to ensure responsiveness and visual consistency.

These changes collectively enhance the usability and clarity of the AI chat interface, providing a more intuitive experience for users.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-08-21 22:05:35 +08:00
parent c2ea3828fe
commit 1f340595b6
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
13 changed files with 319 additions and 177 deletions

View File

@ -51,7 +51,7 @@ const FeedAnalytics = ({ data }: { data: AnalyticsData["feedData"] }) => {
</div>
{analytics && (
<div className="@[400px]:grid-cols-2 grid grid-cols-1 gap-4">
<div className="@[600px]:grid-cols-2 @[400px]:grid-cols-2 grid grid-cols-1 gap-4">
<StatCard title="Views" value={analytics.view || 0} emoji="👀" />
<StatCard
title="Last Checked"
@ -258,7 +258,7 @@ const AIDisplayAnalyticsPartBase = ({
}
return (
<Card className="mx-auto mb-2 w-full max-w-4xl">
<Card className="w-[calc(var(--ai-chat-layout-width,65ch)] mx-auto mb-2">
<CardHeader>
<CardTitle className="text-text flex items-center gap-2 text-xl font-semibold">
<span className="text-lg">📊</span>
@ -279,5 +279,5 @@ export const AIDisplayAnalyticsPart = withDisplayStateHandler<AIDisplayAnalytics
title: "Analytics",
loadingDescription: "Fetching analytics data...",
errorTitle: "Analytics Error",
maxWidth: "max-w-4xl",
maxWidth: "max-w-full",
})(AIDisplayAnalyticsPartBase)

View File

@ -65,7 +65,7 @@ export const AIDisplayFlowPart = toolMemo(
}, [nodes, edges, colorMode, present])
return (
<div className="group relative my-2 aspect-[4/3] w-[calc(var(--ai-chat-layout-width,65ch)-120px)] max-w-prose overflow-hidden rounded-md">
<div className="group relative my-2 aspect-[4/3] w-[calc(var(--ai-chat-layout-width,65ch))] max-w-full overflow-hidden rounded-md">
<ReactFlow
colorMode={colorMode}
nodes={nodes}

View File

@ -17,6 +17,7 @@ import {
AIChatMessage,
AIChatWaitingIndicator,
} from "~/modules/ai-chat/components/message/AIChatMessage"
import { UserChatMessage } from "~/modules/ai-chat/components/message/UserChatMessage"
import { useAutoScroll } from "~/modules/ai-chat/hooks/useAutoScroll"
import { useLoadMessages } from "~/modules/ai-chat/hooks/useLoadMessages"
import { useMainEntryId } from "~/modules/ai-chat/hooks/useMainEntryId"
@ -249,7 +250,11 @@ const Messages: FC = () => {
<div className="relative flex min-w-0 flex-1 flex-col">
{messages.map((message) => (
<Suspense key={message.id}>
<AIChatMessage message={message} />
{message.role === "user" ? (
<UserChatMessage message={message} />
) : (
<AIChatMessage message={message} />
)}
</Suspense>
))}
</div>

View File

@ -1,21 +1,18 @@
import { createDefaultLexicalEditor } from "@follow/components/ui/lexical-rich-editor/editor.js"
import { cn, stopPropagation, thenable } from "@follow/utils"
import { stopPropagation, thenable } from "@follow/utils"
import type { UIDataTypes, UIMessage } from "ai"
import type { LexicalEditor, SerializedEditorState } from "lexical"
import type { LexicalEditor } from "lexical"
import { m } from "motion/react"
import * as React from "react"
import { toast } from "sonner"
import { copyToClipboard } from "~/lib/clipboard"
import { useEditingMessageId, useSetEditingMessageId } from "~/modules/ai-chat/atoms/session"
import { useChatActions } from "~/modules/ai-chat/store/hooks"
import type { BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types"
import { MentionPlugin } from "../../editor"
import type { RichTextPart } from "../../types/ChatSession"
import { convertLexicalToMarkdown } from "../../utils/lexical-markdown"
import { AIMessageParts } from "./AIMessageParts"
import { EditableMessage } from "./EditableMessage"
export interface ChatMessage {
id: string
@ -26,31 +23,11 @@ export interface ChatMessage {
interface AIChatMessageProps {
message: UIMessage<BizUIMetadata, UIDataTypes, BizUITools>
onEdit?: (messageId: string) => void
streaming?: boolean
}
export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message }) => {
if (message.parts.length === 0) {
throw thenable
}
const chatActions = useChatActions()
const messageId = message.id
const [isHovered, setIsHovered] = React.useState(false)
const editingMessageId = useEditingMessageId()
const setEditingMessageId = useSetEditingMessageId()
const isEditing = editingMessageId === messageId
const isUserMessage = message.role === "user"
const handleEdit = React.useCallback(() => {
if (isUserMessage) {
setEditingMessageId(messageId)
}
}, [isUserMessage, messageId, setEditingMessageId])
const getMessageMarkdownFormat = React.useCallback(() => {
// Utility function for converting message to markdown
const useMessageMarkdownFormat = (message: UIMessage<BizUIMetadata, UIDataTypes, BizUITools>) => {
return React.useCallback(() => {
let content = ""
for (const part of message.parts) {
let lexicalEditor: LexicalEditor | null = null
@ -78,38 +55,16 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message
}
return content
}, [message.parts])
}
const handleSaveEdit = React.useCallback(
(newState: SerializedEditorState, editor: LexicalEditor) => {
const messageContent = convertLexicalToMarkdown(editor)
const messages = chatActions.getMessages()
const messageIndex = messages.findIndex((msg) => msg.id === messageId)
if (messageIndex !== -1) {
const messagesToKeep = messages.slice(0, messageIndex)
const nextMessage = messages[messageIndex]!
chatActions.setMessages(messagesToKeep)
// AI message component
export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message }) => {
if (message.parts.length === 0) {
throw thenable
}
const richTextPart = nextMessage.parts.find(
(part) => part.type === "data-rich-text",
) as RichTextPart
if (richTextPart) {
richTextPart.data = {
state: newState,
text: messageContent,
}
}
// Send the edited message
chatActions.sendMessage(nextMessage)
}
setEditingMessageId(null)
},
[chatActions, messageId, setEditingMessageId],
)
const handleCancelEdit = React.useCallback(() => {
setEditingMessageId(null)
}, [setEditingMessageId])
const [isHovered, setIsHovered] = React.useState(false)
const getMessageMarkdownFormat = useMessageMarkdownFormat(message)
const handleCopy = React.useCallback(async () => {
const messageContent = getMessageMarkdownFormat()
@ -121,86 +76,42 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message
}
}, [getMessageMarkdownFormat])
const handleRetry = React.useCallback(() => {
chatActions.regenerate({ messageId })
}, [chatActions, messageId])
return (
<div
onContextMenu={stopPropagation}
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"} group`}
className="group flex justify-start"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="text-text relative flex max-w-[calc(100%-1rem)] flex-col gap-2">
{/* Show editable message if editing */}
{isEditing && isUserMessage ? (
<EditableMessage
messageId={messageId}
parts={message.parts}
onSave={handleSaveEdit}
onCancel={handleCancelEdit}
/>
) : (
<>
{/* Normal message display */}
<div
className={cn(
`text-text backdrop-blur-sm`,
message.role === "user" && "rounded-2xl bg-gray-100 px-4 py-2.5 dark:bg-gray-800",
)}
>
<div className={`flex select-text flex-col gap-2 text-sm`}>
<AIMessageParts message={message} />
</div>
</div>
<div className="text-text relative flex max-w-full flex-col gap-2">
{/* Normal message display */}
<div className="text-text">
<div className="flex select-text flex-col gap-2 text-sm">
<AIMessageParts message={message} />
</div>
</div>
{/* Action buttons */}
<m.div
className={`absolute bottom-1 flex ${message.role === "user" ? "right-0" : "left-0"} gap-1`}
initial={{ opacity: 0 }}
animate={{
opacity: isHovered ? 1 : 0,
}}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{message.role === "user" ? (
<>
<button
type="button"
onClick={handleEdit}
className="text-text hover:bg-fill-secondary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
title="Edit message"
>
<i className="i-mgc-edit-cute-re size-3" />
<span>Edit</span>
</button>
<button
type="button"
onClick={handleRetry}
className="text-text hover:bg-fill-secondary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
title="Retry"
>
<i className="i-mgc-refresh-2-cute-re size-3" />
<span>Retry</span>
</button>
</>
) : (
<button
type="button"
onClick={handleCopy}
className="text-text hover:bg-fill-tertiary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
title="Copy message"
>
<i className="i-mgc-copy-2-cute-re size-3" />
<span>Copy</span>
</button>
)}
</m.div>
{/* Action buttons */}
<m.div
className="absolute bottom-1 left-0 flex gap-1"
initial={{ opacity: 0 }}
animate={{
opacity: isHovered ? 1 : 0,
}}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<button
type="button"
onClick={handleCopy}
className="text-text hover:bg-fill-tertiary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
title="Copy message"
>
<i className="i-mgc-copy-2-cute-re size-3" />
<span>Copy</span>
</button>
</m.div>
<div className="h-6" />
</>
)}
<div className="h-6" />
</div>
</div>
)

View File

@ -342,7 +342,7 @@ export const AIMarkdownStreamingMessage = memo(
className?: string
isProcessing?: boolean
}) => {
const className = `prose dark:prose-invert text-sm
const className = `prose max-w-full dark:prose-invert text-sm
prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-h4:text-base prose-h5:text-base prose-h6:text-sm
prose-li:list-disc prose-li:marker:text-accent prose-hr:border-border prose-hr:mx-8`

View File

@ -4,7 +4,6 @@ import type { ToolUIPart } from "ai"
import * as React from "react"
import type {
AIChatContextBlock,
AIDisplayAnalyticsTool,
AIDisplayEntriesTool,
AIDisplayFeedsTool,
@ -19,47 +18,29 @@ import {
AIDisplayFeedsPart,
AIDisplaySubscriptionsPart,
} from "../displays"
import { AIDataBlockPart } from "./AIDataBlockPart"
import { AIMarkdownMessage, AIMarkdownStreamingMessage } from "./AIMarkdownMessage"
import { AIRichTextMessage } from "./AIRichTextMessage"
import { AIMarkdownStreamingMessage } from "./AIMarkdownMessage"
import { ToolInvocationComponent } from "./ToolInvocationComponent"
const LazyAIDisplayFlowPart = React.lazy(() =>
import("../displays/AIDisplayFlowPart").then((mod) => ({ default: mod.AIDisplayFlowPart })),
)
interface MessagePartsProps {
interface AIMessagePartsProps {
message: BizUIMessage
}
export const AIMessageParts: React.FC<MessagePartsProps> = React.memo(({ message }) => {
export const AIMessageParts: React.FC<AIMessagePartsProps> = React.memo(({ message }) => {
return message.parts.map((part, index) => {
const partKey = `${message.id}-${index}`
switch (part.type) {
case "text": {
if (message.role === "assistant")
return (
<AIMarkdownStreamingMessage
isProcessing={message.metadata?.totalTokens === undefined}
key={partKey}
text={part.text}
className={"text-text"}
/>
)
return <AIMarkdownMessage key={partKey} text={part.text} />
}
case "data-block": {
return <AIDataBlockPart key={partKey} blocks={part.data as AIChatContextBlock[]} />
}
case "data-rich-text": {
return (
<AIRichTextMessage
<AIMarkdownStreamingMessage
isProcessing={message.metadata?.totalTokens === undefined}
key={partKey}
data={part.data as { state: string; text: string }}
className="text-text"
text={part.text}
className={"text-text"}
/>
)
}

View File

@ -1,3 +1,10 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@follow/components/ui/accordion/index.js"
import { JsonHighlighter } from "@follow/components/ui/json-highlighter/index.js"
import type { ToolUIPart } from "ai"
import { getToolName } from "ai"
import * as React from "react"
@ -10,23 +17,77 @@ export const ToolInvocationComponent: React.FC<ToolInvocationComponentProps> = R
({ part }) => {
const toolName = getToolName(part)
const hasError = "errorText" in part && part.errorText
const hasResult = "output" in part && part.output
const hasArgs = "input" in part && part.input
// Generate a unique value for this accordion item
const accordionValue = `tool-${"toolCallId" in part ? part.toolCallId : Math.random()}`
return (
<div
className={`min-w-0 max-w-prose text-left ${hasError ? "border-red/30" : "border-border"}`}
className={`min-w-0 max-w-full text-left ${hasError ? "border-red/30" : "border-border"}`}
>
<div className="w-[9999px] max-w-[calc(var(--ai-chat-layout-width,65ch)_-120px)]" />
<div className="flex h-6 min-w-0 flex-1 items-center">
<div className="flex items-center gap-2 text-xs">
<i className={hasError ? "i-mgc-close-cute-re text-red" : "i-mingcute-tool-line"} />
<span className="text-text-secondary">
{hasError ? "Tool Failed:" : "Tool Calling:"}
</span>
<h4 className={`truncate font-medium ${hasError ? "text-red" : "text-text"}`}>
{toolName}
</h4>
</div>
</div>
<div className="max-w-[calc(var(--ai-chat-layout-width,65ch)] w-[9999px]" />
<Accordion type="single" collapsible className="w-full">
<AccordionItem value={accordionValue} className="group border-none">
<AccordionTrigger
className="group flex h-6 min-w-0 flex-1 items-center justify-between py-0 hover:no-underline"
chevron={false}
>
<div className="flex items-center gap-2 text-xs">
<i className={hasError ? "i-mgc-close-cute-re text-red" : "i-mgc-tool-cute-re"} />
<span className="text-text-secondary">
{hasError ? "Tool Failed:" : "Tool Called:"}
</span>
<h4 className={`truncate font-medium ${hasError ? "text-red" : "text-text"}`}>
{toolName}
</h4>
</div>
{/* Custom arrow that only shows on hover */}
<div className="ml-auto flex items-center justify-center opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<i className="i-mgc-right-cute-re size-3 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
</div>
</AccordionTrigger>
<AccordionContent className="pb-0 pt-2">
<div className="space-y-2 text-xs">
{/* Show tool arguments if available */}
{hasArgs ? (
<div>
<div className="text-text-secondary mb-1 font-medium">Arguments:</div>
<JsonHighlighter
className="text-text-tertiary bg-fill-secondary overflow-x-auto rounded p-2 text-[11px]"
json={JSON.stringify(part.input, null, 2)}
/>
</div>
) : null}
{/* Show tool result if available */}
{hasResult ? (
<div>
<div className="text-text-secondary mb-1 font-medium">Result:</div>
<JsonHighlighter
className="text-text-tertiary bg-fill-secondary overflow-x-auto rounded p-2 text-[11px]"
json={JSON.stringify(part.output, null, 2)}
/>
</div>
) : null}
{/* Show error if available */}
{hasError && "errorText" in part ? (
<div>
<div className="text-red mb-1 font-medium">Error:</div>
<pre className="text-red bg-red/10 overflow-x-auto rounded p-2 text-[11px]">
{String(part.errorText)}
</pre>
</div>
) : null}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)
},

View File

@ -0,0 +1,133 @@
import { stopPropagation, thenable } from "@follow/utils"
import type { UIDataTypes, UIMessage } from "ai"
import type { LexicalEditor, SerializedEditorState } from "lexical"
import { m } from "motion/react"
import * as React from "react"
import { useEditingMessageId, useSetEditingMessageId } from "~/modules/ai-chat/atoms/session"
import { useChatActions } from "~/modules/ai-chat/store/hooks"
import type { BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types"
import type { RichTextPart } from "../../types/ChatSession"
import { convertLexicalToMarkdown } from "../../utils/lexical-markdown"
import { EditableMessage } from "./EditableMessage"
import { UserMessageParts } from "./UserMessageParts"
interface UserChatMessageProps {
message: UIMessage<BizUIMetadata, UIDataTypes, BizUITools>
}
export const UserChatMessage: React.FC<UserChatMessageProps> = React.memo(({ message }) => {
if (message.parts.length === 0) {
throw thenable
}
const chatActions = useChatActions()
const messageId = message.id
const [isHovered, setIsHovered] = React.useState(false)
const editingMessageId = useEditingMessageId()
const setEditingMessageId = useSetEditingMessageId()
const isEditing = editingMessageId === messageId
const handleEdit = React.useCallback(() => {
setEditingMessageId(messageId)
}, [messageId, setEditingMessageId])
const handleSaveEdit = React.useCallback(
(newState: SerializedEditorState, editor: LexicalEditor) => {
const messageContent = convertLexicalToMarkdown(editor)
const messages = chatActions.getMessages()
const messageIndex = messages.findIndex((msg) => msg.id === messageId)
if (messageIndex !== -1) {
const messagesToKeep = messages.slice(0, messageIndex)
const nextMessage = messages[messageIndex]!
chatActions.setMessages(messagesToKeep)
const richTextPart = nextMessage.parts.find(
(part) => part.type === "data-rich-text",
) as RichTextPart
if (richTextPart) {
richTextPart.data = {
state: newState,
text: messageContent,
}
}
// Send the edited message
chatActions.sendMessage(nextMessage)
}
setEditingMessageId(null)
},
[chatActions, messageId, setEditingMessageId],
)
const handleCancelEdit = React.useCallback(() => {
setEditingMessageId(null)
}, [setEditingMessageId])
const handleRetry = React.useCallback(() => {
chatActions.regenerate({ messageId })
}, [chatActions, messageId])
return (
<div
onContextMenu={stopPropagation}
className="group flex justify-end"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="text-text relative flex max-w-[calc(100%-1rem)] flex-col gap-2">
{/* Show editable message if editing */}
{isEditing ? (
<EditableMessage
messageId={messageId}
parts={message.parts}
onSave={handleSaveEdit}
onCancel={handleCancelEdit}
/>
) : (
<>
{/* Normal message display */}
<div className="text-text rounded-2xl bg-gray-100 px-4 py-2.5 backdrop-blur-sm dark:bg-gray-800">
<div className="flex select-text flex-col gap-2 text-sm">
<UserMessageParts message={message} />
</div>
</div>
{/* Action buttons */}
<m.div
className="absolute bottom-1 right-0 flex gap-1"
initial={{ opacity: 0 }}
animate={{
opacity: isHovered ? 1 : 0,
}}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<button
type="button"
onClick={handleEdit}
className="text-text hover:bg-fill-secondary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
title="Edit message"
>
<i className="i-mgc-edit-cute-re size-3" />
<span>Edit</span>
</button>
<button
type="button"
onClick={handleRetry}
className="text-text hover:bg-fill-secondary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
title="Retry"
>
<i className="i-mgc-refresh-2-cute-re size-3" />
<span>Retry</span>
</button>
</m.div>
<div className="h-6" />
</>
)}
</div>
</div>
)
})

View File

@ -0,0 +1,43 @@
import * as React from "react"
import type { AIChatContextBlock, BizUIMessage } from "~/modules/ai-chat/store/types"
import { AIDataBlockPart } from "./AIDataBlockPart"
import { AIMarkdownMessage } from "./AIMarkdownMessage"
import { AIRichTextMessage } from "./AIRichTextMessage"
interface UserMessagePartsProps {
message: BizUIMessage
}
export const UserMessageParts: React.FC<UserMessagePartsProps> = React.memo(({ message }) => {
return message.parts.map((part, index) => {
const partKey = `${message.id}-${index}`
switch (part.type) {
case "text": {
return <AIMarkdownMessage key={partKey} text={part.text} />
}
case "data-block": {
return <AIDataBlockPart key={partKey} blocks={part.data as AIChatContextBlock[]} />
}
case "data-rich-text": {
return (
<AIRichTextMessage
key={partKey}
data={part.data as { state: string; text: string }}
className="text-text"
/>
)
}
default: {
return null
}
}
})
})
UserMessageParts.displayName = "UserMessageParts"

View File

@ -67,7 +67,12 @@ const AIChatFloatingPanelInner: FC<AIChatFloatingPanelProps> = ({ className, ...
exit={{ scale: 0.92, y: 100, opacity: 0 }}
transition={Spring.presets.smooth}
className="fixed z-50"
style={{ left: floatingState.x, top: floatingState.y }}
style={{
left: floatingState.x,
top: floatingState.y,
// @ts-expect-error
"--ai-chat-layout-width": `${floatingState.width}px`,
}}
>
<div className="relative">
<Resizable

View File

@ -186,6 +186,7 @@ const AIEntryLayoutImpl = () => {
}}
/>
<AIChatLayout
key="ai-chat-layout"
style={
{ width: position, "--ai-chat-layout-width": `${position}px` } as React.CSSProperties
}
@ -194,7 +195,7 @@ const AIEntryLayoutImpl = () => {
)}
{/* Floating panel - renders outside layout flow */}
{panelStyle === AIChatPanelStyle.Floating && <AIChatLayout />}
{panelStyle === AIChatPanelStyle.Floating && <AIChatLayout key="ai-chat-layout" />}
</div>
)
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="m8.64 2.425-.454.078c-.448.074-.746.214-1.031.486-.648.616-.717 1.624-.167 2.422.057.082.713.71 1.457 1.394 1.076.989 1.362 1.273 1.391 1.381.029.102.014.254-.061.624a4.643 4.643 0 0 1-.147.585c-.087.178-.233.243-.792.353-.467.092-.561.1-.666.056-.075-.031-.599-.569-1.352-1.388C5.969 7.493 5.515 7.03 5.35 6.92c-1.057-.706-2.476-.197-2.784 1-.205.799-.207 2.004-.006 2.929a7.033 7.033 0 0 0 3.683 4.775 6.929 6.929 0 0 0 3.654.733c.478-.036 1.193-.157 1.406-.238a.586.586 0 0 1 .137-.039c.019 0 .92 1.045 2.001 2.323 1.518 1.792 2.036 2.377 2.269 2.562.77.612 1.791.911 2.73.798.932-.112 1.668-.463 2.287-1.09.741-.751 1.1-1.682 1.061-2.753-.033-.937-.303-1.643-.898-2.351-.098-.116-1.213-1.087-2.478-2.157-2.25-1.905-2.3-1.949-2.277-2.069.012-.068.069-.339.125-.603.375-1.753-.016-3.708-1.05-5.26-.43-.645-1.253-1.468-1.89-1.89a7.287 7.287 0 0 0-2.56-1.072c-.331-.069-.59-.09-1.22-.1-.44-.006-.845-.003-.9.007m1.64 2.041c1.764.308 3.288 1.613 3.85 3.294.21.629.25.893.249 1.64-.002.65-.011.738-.13 1.225-.22.898-.149 1.421.279 2.046.071.103 1.035.948 2.427 2.126 1.27 1.075 2.367 2.02 2.437 2.1.271.309.417.715.413 1.151-.008.829-.527 1.477-1.355 1.693a1.719 1.719 0 0 1-1.505-.347c-.084-.07-1.032-1.165-2.105-2.433-1.073-1.267-2.012-2.351-2.086-2.406-.589-.445-1.217-.547-2.074-.336-.648.159-1.162.204-1.696.148-1.055-.11-1.894-.446-2.706-1.086-1.128-.888-1.796-2.216-1.864-3.701-.013-.286-.008-.577.011-.647l.035-.127 1.036 1.127c.57.62 1.135 1.212 1.256 1.315.255.218.691.441 1.008.516.533.126 1.639-.009 2.36-.288.532-.205.968-.597 1.262-1.132.164-.298.215-.47.358-1.194.108-.549.127-1.017.056-1.358-.063-.299-.317-.815-.522-1.059-.096-.114-.678-.67-1.294-1.235a41.618 41.618 0 0 1-1.133-1.061c-.032-.084.896-.065 1.433.029" fill="#10161F" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -46,6 +46,7 @@ function AccordionItem({ className, children, ...props }: AccordionItemProps) {
)}
>
<AccordionPrimitive.Item
data-state={isOpen ? "open" : "closed"}
data-slot="accordion-item"
className={cn("border-b", className)}
{...props}