setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
-
- {/* Show editable message if editing */}
- {isEditing && isUserMessage ? (
-
- ) : (
- <>
- {/* Normal message display */}
-
+
+ {/* Normal message display */}
+
- {/* Action buttons */}
-
- {message.role === "user" ? (
- <>
-
-
- >
- ) : (
-
- )}
-
+ {/* Action buttons */}
+
+
+
-
- >
- )}
+
)
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx
index d23f8d566..f80250d9d 100644
--- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx
+++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx
@@ -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`
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx
index 325c0d8bb..381b4d347 100644
--- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx
+++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx
@@ -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
= React.memo(({ message }) => {
+export const AIMessageParts: React.FC = React.memo(({ message }) => {
return message.parts.map((part, index) => {
const partKey = `${message.id}-${index}`
switch (part.type) {
case "text": {
- if (message.role === "assistant")
- return (
-
- )
- return
- }
-
- case "data-block": {
- return
- }
-
- case "data-rich-text": {
return (
-
)
}
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx
index 7964ebc91..d387e591e 100644
--- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx
+++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx
@@ -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 = 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 (
-
-
-
-
-
- {hasError ? "Tool Failed:" : "Tool Calling:"}
-
-
- {toolName}
-
-
-
+
+
+
+
+
+
+
+
+ {hasError ? "Tool Failed:" : "Tool Called:"}
+
+
+ {toolName}
+
+
+
+ {/* Custom arrow that only shows on hover */}
+
+
+
+
+
+
+
+ {/* Show tool arguments if available */}
+ {hasArgs ? (
+
+ ) : null}
+
+ {/* Show tool result if available */}
+ {hasResult ? (
+
+ ) : null}
+
+ {/* Show error if available */}
+ {hasError && "errorText" in part ? (
+
+
Error:
+
+ {String(part.errorText)}
+
+
+ ) : null}
+
+
+
+
)
},
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx
new file mode 100644
index 000000000..fbb7ab845
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx
@@ -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
+}
+
+export const UserChatMessage: React.FC = 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 (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+ {/* Show editable message if editing */}
+ {isEditing ? (
+
+ ) : (
+ <>
+ {/* Normal message display */}
+
+
+ {/* Action buttons */}
+
+
+
+
+
+
+ >
+ )}
+
+
+ )
+})
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserMessageParts.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserMessageParts.tsx
new file mode 100644
index 000000000..4ffab2766
--- /dev/null
+++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserMessageParts.tsx
@@ -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 = React.memo(({ message }) => {
+ return message.parts.map((part, index) => {
+ const partKey = `${message.id}-${index}`
+
+ switch (part.type) {
+ case "text": {
+ return
+ }
+
+ case "data-block": {
+ return
+ }
+
+ case "data-rich-text": {
+ return (
+
+ )
+ }
+
+ default: {
+ return null
+ }
+ }
+ })
+})
+
+UserMessageParts.displayName = "UserMessageParts"
diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFloatingPanel.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFloatingPanel.tsx
index b47e9aefc..dea204b12 100644
--- a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFloatingPanel.tsx
+++ b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFloatingPanel.tsx
@@ -67,7 +67,12 @@ const AIChatFloatingPanelInner: FC = ({ 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`,
+ }}
>
{
}}
/>
{
)}
{/* Floating panel - renders outside layout flow */}
- {panelStyle === AIChatPanelStyle.Floating && }
+ {panelStyle === AIChatPanelStyle.Floating && }
)
}
diff --git a/icons/mgc/tool_cute_re.svg b/icons/mgc/tool_cute_re.svg
new file mode 100644
index 000000000..8f2e65f06
--- /dev/null
+++ b/icons/mgc/tool_cute_re.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/internal/components/src/ui/accordion/index.tsx b/packages/internal/components/src/ui/accordion/index.tsx
index 0108a7756..47cf6f9e8 100644
--- a/packages/internal/components/src/ui/accordion/index.tsx
+++ b/packages/internal/components/src/ui/accordion/index.tsx
@@ -46,6 +46,7 @@ function AccordionItem({ className, children, ...props }: AccordionItemProps) {
)}
>