feat(ai): update ai-tools and optimize chat message components animation
- Bumped version of `@folo-services/ai-tools` from 0.2.31 to 0.2.33 in package.json and pnpm-lock.yaml. - Refactored `AIDisplayFlowPart` to improve error handling and compatibility with older data structures. - Enhanced `AIChatMessage` and `AIMessageParts` components to support new props for better message rendering. - Updated animation styles for chat messages to improve user experience during interactions. This update improves the overall functionality and visual appeal of the AI chat interface. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
1f54e024b9
commit
79bf7a86e5
|
|
@ -121,7 +121,7 @@
|
|||
"@follow/models": "workspace:*",
|
||||
"@follow/types": "workspace:*",
|
||||
"@follow/utils": "workspace:*",
|
||||
"@folo-services/ai-tools": "0.2.31",
|
||||
"@folo-services/ai-tools": "0.2.33",
|
||||
"@types/node": "24.0.10",
|
||||
"@vite-pwa/assets-generator": "1.0.0",
|
||||
"fake-indexeddb": "6.0.1",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import "@xyflow/react/dist/style.css"
|
||||
|
||||
import { useIsDark } from "@follow/hooks"
|
||||
import { thenable } from "@follow/utils"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import type { ToolWithState } from "@folo-services/ai-tools"
|
||||
import { Background, Controls, ReactFlow } from "@xyflow/react"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
|
||||
import type { AIDisplayFlowTool } from "../../store/types"
|
||||
import { toolMemo } from "./share"
|
||||
|
||||
const FlowPreviewModal = ({
|
||||
|
|
@ -41,64 +42,63 @@ const FlowPreviewModal = ({
|
|||
)
|
||||
}
|
||||
|
||||
export const AIDisplayFlowPart = toolMemo(
|
||||
({ part, loadingElement }: { part: AIDisplayFlowTool; loadingElement: React.ReactNode }) => {
|
||||
if (!part.input) return loadingElement
|
||||
if (part.state === "input-streaming") {
|
||||
return loadingElement
|
||||
}
|
||||
export const AIDisplayFlowPart = toolMemo(({ part }: { part: ToolWithState<any> }) => {
|
||||
if (!part.input) throw thenable
|
||||
if (part.state === "input-streaming") {
|
||||
throw thenable
|
||||
}
|
||||
|
||||
const { nodes, edges } = part.input.flowChart as any
|
||||
const colorMode = useIsDark() ? "dark" : "light"
|
||||
const { present } = useModalStack()
|
||||
// `part.input.flowChart` to make compatible with old data
|
||||
const { nodes, edges } = part.input.flowChart || (part.input.schema.flowChart as any)
|
||||
const colorMode = useIsDark() ? "dark" : "light"
|
||||
const { present } = useModalStack()
|
||||
|
||||
const handleOpenModal = useCallback(() => {
|
||||
present({
|
||||
title: "Flow Chart Preview",
|
||||
content: () => <FlowPreviewModal nodes={nodes} edges={edges} colorMode={colorMode} />,
|
||||
max: true,
|
||||
canClose: true,
|
||||
clickOutsideToDismiss: false,
|
||||
modalContentClassName: "p-0 h-full",
|
||||
modalClassName: "h-[90vh] w-[90vw]",
|
||||
})
|
||||
}, [nodes, edges, colorMode, present])
|
||||
const handleOpenModal = useCallback(() => {
|
||||
present({
|
||||
title: "Flow Chart Preview",
|
||||
content: () => <FlowPreviewModal nodes={nodes} edges={edges} colorMode={colorMode} />,
|
||||
max: true,
|
||||
canClose: true,
|
||||
clickOutsideToDismiss: false,
|
||||
modalContentClassName: "p-0 h-full",
|
||||
modalClassName: "h-[90vh] w-[90vw]",
|
||||
})
|
||||
}, [nodes, edges, colorMode, present])
|
||||
|
||||
return (
|
||||
<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}
|
||||
edges={edges}
|
||||
fitView
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
preventScrolling={false}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
return (
|
||||
<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}
|
||||
edges={edges}
|
||||
fitView
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
preventScrolling={false}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
|
||||
{/* Expand/Preview button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenModal}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 flex items-center gap-1.5 rounded-lg px-2.5 py-1.5",
|
||||
"bg-material-thick text-text-secondary text-sm font-medium",
|
||||
"opacity-0 transition-all duration-200 group-hover:opacity-100",
|
||||
"hover:bg-material-medium hover:text-text focus:opacity-100",
|
||||
"focus:ring-blue focus:outline-none focus:ring-2 focus:ring-offset-1",
|
||||
)}
|
||||
title="Open in full screen"
|
||||
>
|
||||
<i className="i-mgc-external-link-cute-re size-4" />
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
{/* Expand/Preview button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenModal}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 flex items-center gap-1.5 rounded-lg px-2.5 py-1.5",
|
||||
"bg-material-thick text-text-secondary text-sm font-medium",
|
||||
"opacity-0 transition-all duration-200 group-hover:opacity-100",
|
||||
"hover:bg-material-medium hover:text-text focus:opacity-100",
|
||||
"focus:ring-blue focus:outline-none focus:ring-2 focus:ring-offset-1",
|
||||
)}
|
||||
title="Open in full screen"
|
||||
>
|
||||
<i className="i-mgc-external-link-cute-re size-4" />
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import { ActionButton } from "@follow/components/ui/button/index.js"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { memo, useCallback, useMemo, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useAISettingValue } from "~/atoms/settings/ai"
|
||||
import { DropdownMenu, DropdownMenuTrigger } from "~/components/ui/dropdown-menu/dropdown-menu"
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useFileUploadWithDefaults } from "~/modules/ai-chat/hooks/useFileUpload"
|
||||
import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext"
|
||||
import { SUPPORTED_MIME_ACCEPT } from "~/modules/ai-chat/utils/file-validation"
|
||||
import { useCanCreateNewAITask } from "~/modules/ai-task/query"
|
||||
|
||||
import { AITaskModal } from "../../../ai-task/components/ai-task-modal"
|
||||
import { ContextBlock } from "../context-bar/blocks"
|
||||
import { ContextMenuContent, ShortcutsMenuContent } from "../context-bar/menus"
|
||||
|
||||
|
|
@ -21,8 +16,6 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) =>
|
|||
const { shortcuts } = useAISettingValue()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const { handleFileInputChange } = useFileUploadWithDefaults()
|
||||
const { present } = useModalStack()
|
||||
const canCreateNewTask = useCanCreateNewAITask()
|
||||
|
||||
// Filter enabled shortcuts
|
||||
const enabledShortcuts = useMemo(
|
||||
|
|
@ -34,18 +27,6 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) =>
|
|||
fileInputRef.current?.click()
|
||||
}, [])
|
||||
|
||||
const handleScheduleActionClick = () => {
|
||||
if (!canCreateNewTask) {
|
||||
toast.error("Please remove an existing task before creating a new one.")
|
||||
return
|
||||
}
|
||||
present({
|
||||
title: "New AI Task",
|
||||
canClose: true,
|
||||
content: () => <AITaskModal showSettingsTip />,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap items-center gap-2 px-4 py-3", className)}>
|
||||
{/* Add Context Button */}
|
||||
|
|
@ -97,14 +78,6 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) =>
|
|||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<ActionButton
|
||||
className="bg-material-medium hover:bg-material-thin border-border text-text-secondary hover:text-text-secondary flex size-7 items-center justify-center rounded-md border transition-colors"
|
||||
tooltip="Schedule Action"
|
||||
onClick={handleScheduleActionClick}
|
||||
>
|
||||
<i className="i-mgc-calendar-time-add-cute-re size-3.5" />
|
||||
</ActionButton>
|
||||
|
||||
{/* Context Blocks */}
|
||||
{blocks.map((block) => (
|
||||
<ContextBlock key={block.id} block={block} />
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const ChatHeaderLayout = ({
|
|||
[chatActions],
|
||||
)
|
||||
|
||||
const maskImage = `linear-gradient(to bottom, black 0%, black 75%, transparent 100%)`
|
||||
const maskImage = `linear-gradient(to bottom, black 0%, black 90%, transparent 100%)`
|
||||
return (
|
||||
<div className="absolute inset-x-0 top-0 z-[1] h-12">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -253,15 +253,18 @@ const Messages: FC = () => {
|
|||
|
||||
return (
|
||||
<div className="relative flex min-w-0 flex-1 flex-col">
|
||||
{messages.map((message) => (
|
||||
<Suspense key={message.id}>
|
||||
{message.role === "user" ? (
|
||||
<UserChatMessage message={message} />
|
||||
) : (
|
||||
<AIChatMessage message={message} />
|
||||
)}
|
||||
</Suspense>
|
||||
))}
|
||||
{messages.map((message, index) => {
|
||||
const isLastMessage = index === messages.length - 1
|
||||
return (
|
||||
<Suspense key={message.id}>
|
||||
{message.role === "user" ? (
|
||||
<UserChatMessage message={message} />
|
||||
) : (
|
||||
<AIChatMessage message={message} isLastMessage={isLastMessage} />
|
||||
)}
|
||||
</Suspense>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Spring } from "@follow/components/constants/spring.js"
|
||||
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
|
||||
import { cn } from "@follow/utils"
|
||||
import { ExceptionCodeMap } from "@follow-app/client-sdk"
|
||||
import { m } from "motion/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { useI18n } from "~/hooks/common/useI18n"
|
||||
|
|
@ -12,6 +11,7 @@ interface CollapsibleErrorProps {
|
|||
className?: string
|
||||
collapsedHeight?: string
|
||||
icon?: string
|
||||
expandedMaxHeight?: string
|
||||
}
|
||||
|
||||
interface ErrorData {
|
||||
|
|
@ -27,6 +27,7 @@ export const CollapsibleError: React.FC<CollapsibleErrorProps> = ({
|
|||
className,
|
||||
collapsedHeight = "48px",
|
||||
icon = "i-mgc-alert-cute-fi",
|
||||
expandedMaxHeight = "240px",
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false)
|
||||
const t = useI18n()
|
||||
|
|
@ -159,56 +160,42 @@ export const CollapsibleError: React.FC<CollapsibleErrorProps> = ({
|
|||
onMouseEnter={() => setIsExpanded(true)}
|
||||
onMouseLeave={() => setIsExpanded(false)}
|
||||
>
|
||||
<m.div
|
||||
initial={false}
|
||||
animate={{
|
||||
height: isExpanded ? "auto" : collapsedHeight,
|
||||
}}
|
||||
transition={Spring.presets.snappy}
|
||||
className="overflow-hidden"
|
||||
<div
|
||||
className={
|
||||
"border-red/20 shadow-red/5 dark:shadow-red/10 backdrop-blur-background bg-mix-red-20 relative overflow-hidden rounded-xl transition-all duration-200"
|
||||
}
|
||||
>
|
||||
{/* Collapsed Content */}
|
||||
<div
|
||||
className={
|
||||
"border-red/20 shadow-red/5 dark:shadow-red/10 backdrop-blur-background bg-mix-red-20 relative overflow-hidden rounded-xl transition-all duration-200"
|
||||
}
|
||||
className="relative z-10 flex items-center gap-3 p-3"
|
||||
style={{ minHeight: collapsedHeight }}
|
||||
>
|
||||
{/* Collapsed Content */}
|
||||
<div className="relative z-10 flex items-center gap-3 p-3">
|
||||
<m.div
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-red/20 flex size-6 flex-shrink-0 items-center justify-center rounded-full"
|
||||
>
|
||||
<i className={cn(icon, "text-red size-3")} />
|
||||
</m.div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-red text-sm font-medium">{getErrorTitle()}</div>
|
||||
</div>
|
||||
<m.span
|
||||
animate={{
|
||||
opacity: isExpanded ? 0 : 1,
|
||||
scale: isExpanded ? 0.8 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="text-text-tertiary text-xs"
|
||||
>
|
||||
hover to expand
|
||||
</m.span>
|
||||
<div className="bg-red/20 flex size-6 flex-shrink-0 items-center justify-center rounded-full transition-colors duration-200">
|
||||
<i className={cn(icon, "text-red size-3")} />
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
<m.div
|
||||
animate={{
|
||||
opacity: isExpanded ? 1 : 0,
|
||||
height: isExpanded ? "auto" : 0,
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-red text-sm font-medium">{getErrorTitle()}</div>
|
||||
</div>
|
||||
<span
|
||||
className="text-text-tertiary text-xs transition-all duration-150"
|
||||
style={{
|
||||
opacity: isExpanded ? 0 : 1,
|
||||
transform: isExpanded ? "scale(0.95)" : "scale(1)",
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
hover to expand
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
<div
|
||||
className="overflow-hidden transition-[max-height,opacity] duration-200 ease-out"
|
||||
style={{ maxHeight: isExpanded ? expandedMaxHeight : "0px", opacity: isExpanded ? 1 : 0 }}
|
||||
aria-hidden={!isExpanded}
|
||||
>
|
||||
<ScrollArea focusable={false} viewportProps={{ style: { maxHeight: expandedMaxHeight } }}>
|
||||
<div className="border-red/20 bg-red/5 border-t px-3 pb-3">
|
||||
<div className="text-red/80 bg-red/10 mt-2 rounded-md p-3 text-xs leading-relaxed">
|
||||
<div className="text-red/80 bg-red/10 mt-2 cursor-text select-text break-all rounded-md p-3 text-xs leading-relaxed">
|
||||
{displayMessage as string}
|
||||
</div>
|
||||
{contextualInfo && (
|
||||
|
|
@ -217,9 +204,9 @@ export const CollapsibleError: React.FC<CollapsibleErrorProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</m.div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</m.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,17 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu/dropdown-menu"
|
||||
import { useDialog } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useChatActions, useCurrentChatId } from "~/modules/ai-chat/store/hooks"
|
||||
import { AIChatSessionService } from "~/modules/ai-chat-session"
|
||||
import {
|
||||
useAIChatSessionListQuery,
|
||||
useDeleteAIChatSessionMutation,
|
||||
} from "~/modules/ai-chat-session/query"
|
||||
import { AITaskModal, useCanCreateNewAITask } from "~/modules/ai-task"
|
||||
|
||||
import { AIPersistService } from "../../services"
|
||||
|
||||
|
|
@ -77,7 +79,7 @@ const SessionItem = ({ session, onClick, onDelete, isLoading }: SessionItemProps
|
|||
const EmptyState = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<i className="i-mgc-inbox-cute-re text-text-secondary mb-2 block size-8" />
|
||||
<i className="i-mgc-calendar-time-add-cute-re text-text-secondary mb-2 block size-8" />
|
||||
<p className="text-text-secondary text-sm">All task reports read</p>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -100,6 +102,20 @@ export const TaskReportDropdown = ({ triggerElement, asChild = true }: TaskRepor
|
|||
|
||||
const hasUnreadSessions = unreadSessions.length > 0
|
||||
|
||||
const { present } = useModalStack()
|
||||
const canCreateNewTask = useCanCreateNewAITask()
|
||||
const handleScheduleActionClick = () => {
|
||||
if (!canCreateNewTask) {
|
||||
toast.error("Please remove an existing task before creating a new one.")
|
||||
return
|
||||
}
|
||||
present({
|
||||
title: "New AI Task",
|
||||
canClose: true,
|
||||
content: () => <AITaskModal showSettingsTip />,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSessionSelect = useCallback(
|
||||
async (session: AIChatSession) => {
|
||||
if (session.chatId === currentChatId) return
|
||||
|
|
@ -153,7 +169,7 @@ export const TaskReportDropdown = ({ triggerElement, asChild = true }: TaskRepor
|
|||
|
||||
const defaultTrigger = (
|
||||
<ActionButton tooltip="Task Reports" className="relative">
|
||||
<i className="i-mgc-inbox-cute-re text-text-secondary size-5" />
|
||||
<i className="i-mgc-calendar-time-add-cute-re text-text-secondary size-5" />
|
||||
{hasUnreadSessions && (
|
||||
<span
|
||||
className="bg-accent absolute right-1 top-1 block size-2 rounded-full shadow-[0_0_0_2px_var(--color-bg-default)] dark:shadow-[0_0_0_2px_var(--color-bg-default)]"
|
||||
|
|
@ -195,6 +211,16 @@ export const TaskReportDropdown = ({ triggerElement, asChild = true }: TaskRepor
|
|||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
|
||||
{canCreateNewTask && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleScheduleActionClick}>
|
||||
<i className="i-mgc-add-cute-re mr-2 size-4" />
|
||||
New Task
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface ChatMessage {
|
|||
|
||||
interface AIChatMessageProps {
|
||||
message: BizUIMessage
|
||||
isLastMessage: boolean
|
||||
}
|
||||
|
||||
// Utility function for converting message to markdown
|
||||
|
|
@ -58,61 +59,64 @@ const useMessageMarkdownFormat = (message: BizUIMessage) => {
|
|||
}
|
||||
|
||||
// AI message component
|
||||
export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message }) => {
|
||||
if (message.parts.length === 0) {
|
||||
throw thenable
|
||||
}
|
||||
|
||||
const getMessageMarkdownFormat = useMessageMarkdownFormat(message)
|
||||
|
||||
const handleCopy = React.useCallback(async () => {
|
||||
const messageContent = getMessageMarkdownFormat()
|
||||
try {
|
||||
await copyToClipboard(messageContent)
|
||||
toast.success("Message copied to clipboard")
|
||||
} catch {
|
||||
toast.error("Failed to copy message")
|
||||
export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(
|
||||
({ message, isLastMessage }) => {
|
||||
if (message.parts.length === 0) {
|
||||
throw thenable
|
||||
}
|
||||
}, [getMessageMarkdownFormat])
|
||||
|
||||
return (
|
||||
<div onContextMenu={stopPropagation} className="group flex justify-start">
|
||||
<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} />
|
||||
const getMessageMarkdownFormat = useMessageMarkdownFormat(message)
|
||||
|
||||
const handleCopy = React.useCallback(async () => {
|
||||
const messageContent = getMessageMarkdownFormat()
|
||||
try {
|
||||
await copyToClipboard(messageContent)
|
||||
toast.success("Message copied to clipboard")
|
||||
} catch {
|
||||
toast.error("Failed to copy message")
|
||||
}
|
||||
}, [getMessageMarkdownFormat])
|
||||
|
||||
return (
|
||||
<div onContextMenu={stopPropagation} className="group flex justify-start">
|
||||
<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} isLastMessage={isLastMessage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="absolute -left-2 bottom-1 flex gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="text-text-secondary 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>
|
||||
{message.metadata && (
|
||||
<TokenUsagePill metadata={message.metadata}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-secondary hover:bg-fill-tertiary flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
|
||||
>
|
||||
<i className="i-mgc-information-cute-re size-3" />
|
||||
</button>
|
||||
</TokenUsagePill>
|
||||
)}
|
||||
</div>
|
||||
{/* Action buttons */}
|
||||
<div className="absolute -left-2 bottom-1 right-0 flex gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="text-text-secondary 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>
|
||||
|
||||
<div className="h-6" />
|
||||
{message.metadata && (
|
||||
<TokenUsagePill metadata={message.metadata}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-secondary hover:bg-fill-tertiary absolute right-0 flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
|
||||
>
|
||||
<i className="i-mgc-information-cute-re size-3" />
|
||||
</button>
|
||||
</TokenUsagePill>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const AIChatWaitingIndicator: React.FC = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import "@xyflow/react/dist/style.css"
|
||||
|
||||
import { alwaysFalse } from "@follow/utils"
|
||||
import { ErrorBoundary } from "@sentry/react"
|
||||
import type { ToolUIPart } from "ai"
|
||||
import * as React from "react"
|
||||
|
||||
|
|
@ -29,98 +31,100 @@ const LazyAIDisplayFlowPart = React.lazy(() =>
|
|||
|
||||
interface AIMessagePartsProps {
|
||||
message: BizUIMessage
|
||||
isLastMessage: boolean
|
||||
}
|
||||
|
||||
export const AIMessageParts: React.FC<AIMessagePartsProps> = React.memo(({ message }) => {
|
||||
const [shouldStreamingAnimation, setShouldStreamingAnimation] = React.useState(false)
|
||||
const chatStatus = useChatStatus()
|
||||
export const AIMessageParts: React.FC<AIMessagePartsProps> = React.memo(
|
||||
({ message, isLastMessage }) => {
|
||||
const [shouldStreamingAnimation, setShouldStreamingAnimation] = React.useState(false)
|
||||
const chatStatus = useChatStatus()
|
||||
|
||||
React.useEffect(() => {
|
||||
// Delay 2s to set shouldStreamingAnimation
|
||||
const timerId = setTimeout(() => {
|
||||
setShouldStreamingAnimation(true)
|
||||
}, 2000)
|
||||
return () => clearTimeout(timerId)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
// Delay 2s to set shouldStreamingAnimation
|
||||
const timerId = setTimeout(() => {
|
||||
setShouldStreamingAnimation(true)
|
||||
}, 2000)
|
||||
return () => clearTimeout(timerId)
|
||||
}, [])
|
||||
|
||||
const shouldMessageAnimation = React.useMemo(() => {
|
||||
return chatStatus === "streaming" && shouldStreamingAnimation
|
||||
}, [chatStatus, shouldStreamingAnimation])
|
||||
const shouldMessageAnimation = React.useMemo(() => {
|
||||
return chatStatus === "streaming" && shouldStreamingAnimation && isLastMessage
|
||||
}, [chatStatus, isLastMessage, shouldStreamingAnimation])
|
||||
|
||||
return message.parts.map((part, index) => {
|
||||
const partKey = `${message.id}-${index}`
|
||||
return message.parts.map((part, index) => {
|
||||
const partKey = `${message.id}-${index}`
|
||||
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
return (
|
||||
<AIMarkdownStreamingMessage
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
className={"text-text"}
|
||||
isStreaming={shouldMessageAnimation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
return (
|
||||
<AIMarkdownStreamingMessage
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
className={"text-text"}
|
||||
isStreaming={shouldMessageAnimation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case "reasoning": {
|
||||
return (
|
||||
<AIReasoningPart
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
isStreaming={part.state === "streaming"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "reasoning": {
|
||||
return (
|
||||
<AIReasoningPart
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
isStreaming={part.state === "streaming"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case "tool-displayAnalytics": {
|
||||
return <AIDisplayAnalyticsPart key={partKey} part={part as AIDisplayAnalyticsTool} />
|
||||
}
|
||||
case "tool-displayEntries": {
|
||||
return <AIDisplayEntriesPart key={partKey} part={part as AIDisplayEntriesTool} />
|
||||
}
|
||||
case "tool-displaySubscriptions": {
|
||||
return (
|
||||
<AIDisplaySubscriptionsPart key={partKey} part={part as AIDisplaySubscriptionsTool} />
|
||||
)
|
||||
}
|
||||
case "tool-displayFeeds": {
|
||||
return <AIDisplayFeedsPart key={partKey} part={part as AIDisplayFeedsTool} />
|
||||
}
|
||||
case "tool-displayAnalytics": {
|
||||
return <AIDisplayAnalyticsPart key={partKey} part={part as AIDisplayAnalyticsTool} />
|
||||
}
|
||||
case "tool-displayEntries": {
|
||||
return <AIDisplayEntriesPart key={partKey} part={part as AIDisplayEntriesTool} />
|
||||
}
|
||||
case "tool-displaySubscriptions": {
|
||||
return (
|
||||
<AIDisplaySubscriptionsPart key={partKey} part={part as AIDisplaySubscriptionsTool} />
|
||||
)
|
||||
}
|
||||
case "tool-displayFeeds": {
|
||||
return <AIDisplayFeedsPart key={partKey} part={part as AIDisplayFeedsTool} />
|
||||
}
|
||||
|
||||
case "tool-displayFlowChart": {
|
||||
const loadingElement = (
|
||||
<div className="bg-material-medium my-2 flex aspect-[4/3] w-[calc(var(--ai-chat-layout-width,65ch))] max-w-full items-center justify-center rounded">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-mgc-loading-3-cute-re text-text-secondary size-4 animate-spin" />
|
||||
<span className="text-text-secondary text-sm font-medium">
|
||||
Generating Flow Chart...
|
||||
</span>
|
||||
case "tool-displayFlowChart": {
|
||||
const loadingElement = (
|
||||
<div className="bg-material-medium my-2 flex aspect-[4/3] w-[calc(var(--ai-chat-layout-width,65ch))] max-w-full items-center justify-center rounded">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-mgc-loading-3-cute-re text-text-secondary size-4 animate-spin" />
|
||||
<span className="text-text-secondary text-sm font-medium">
|
||||
Generating Flow Chart...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<React.Suspense fallback={loadingElement} key={partKey}>
|
||||
<LazyAIDisplayFlowPart
|
||||
part={part as AIDisplayFlowTool}
|
||||
loadingElement={loadingElement}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
default: {
|
||||
if (part.type.startsWith("tool-")) {
|
||||
if (part.type.startsWith("tool-chunkBreak")) {
|
||||
return null
|
||||
}
|
||||
return <ToolInvocationComponent key={partKey} part={part as ToolUIPart} />
|
||||
)
|
||||
return (
|
||||
<ErrorBoundary key={partKey} beforeCapture={alwaysFalse}>
|
||||
<React.Suspense fallback={loadingElement}>
|
||||
<LazyAIDisplayFlowPart part={part as AIDisplayFlowTool} />
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
default: {
|
||||
if (part.type.startsWith("tool-")) {
|
||||
if (part.type.startsWith("tool-chunkBreak")) {
|
||||
return null
|
||||
}
|
||||
return <ToolInvocationComponent key={partKey} part={part as ToolUIPart} />
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
AIMessageParts.displayName = "AIMessageParts"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const DEFAULT_ANIMATION = "ft-typeIn 0.5s ease-in-out"
|
||||
export const DEFAULT_ANIMATION = "mask-left-to-right 0.5s ease-in-out"
|
||||
export const ANIMATION_STYLE = {
|
||||
animation: DEFAULT_ANIMATION,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@ export const noop = () => {}
|
|||
// eslint-disable-next-line unicorn/no-thenable
|
||||
export const thenable: any = { then: noop }
|
||||
export const emptyObject = {}
|
||||
|
||||
export const alwaysFalse = () => false
|
||||
|
|
|
|||
|
|
@ -774,8 +774,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../packages/internal/utils
|
||||
'@folo-services/ai-tools':
|
||||
specifier: 0.2.31
|
||||
version: 0.2.31(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
|
||||
specifier: 0.2.33
|
||||
version: 0.2.33(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
|
||||
'@types/node':
|
||||
specifier: 24.0.10
|
||||
version: 24.0.10
|
||||
|
|
@ -4060,18 +4060,18 @@ packages:
|
|||
'@follow-app/client-sdk@0.3.50':
|
||||
resolution: {integrity: sha512-qc8gI5li/v62h1oR8p0bkLh+tlNaEP8jogppoP92v10XktERtd0rrx5grvSQ1pw0mM3Fws4CknEJZeGH9leDaw==}
|
||||
|
||||
'@folo-services/ai-tools@0.2.31':
|
||||
resolution: {integrity: sha512-oqTVMBc6UCLOt/3M9GX4I+DJ8WT2U8Hlok1MN/Sruw8sQu0ZvtoDvkuiJEtssOQAGDifzHm7z8AVmOgNoGrKUg==}
|
||||
'@folo-services/ai-tools@0.2.33':
|
||||
resolution: {integrity: sha512-5Q35xDIT5dt/l1Dn0ApadD8l7NHK8rVJYqemHuENYvaUDPNJ8f7SXfXY5HwaMv/LpV20ln5DwMEHXwjtMdP75g==}
|
||||
|
||||
'@folo-services/constants@0.1.29':
|
||||
resolution: {integrity: sha512-7YH5UQyUoT0STsoAKXTJgdAlvW88RKe5L/yqq4nbovSf2UJm1yEfl+opOYMeYEKBzX3OnixIipPL/1oKQPeh0w==}
|
||||
|
||||
'@folo-services/drizzle@0.1.22':
|
||||
resolution: {integrity: sha512-n4L0bBA5AxqJn5K3k+BTzmtuZh90LAO7G6zALCo3H+BOsnMsQ1OCsQH/xFg7m6ICQ80LJVbHdUN6b4ZKUoQJ/g==}
|
||||
|
||||
'@folo-services/drizzle@0.1.23':
|
||||
resolution: {integrity: sha512-Y4oGzfcYtsh9jdaDZvMh0CMO2tD1c2VSt1gtaZbq9uF17sv45empMI72OdGfWEJBJse7h/P+YGydX7iK63aoIQ==}
|
||||
|
||||
'@folo-services/drizzle@0.1.24':
|
||||
resolution: {integrity: sha512-C2B32AuAIoPihwkowEzQVSCq9jz5gUJOXWSTsh46ICCXIQ+FeDLclGuOS13+3FQ79mGBslUqrRu5chpoeDLE/g==}
|
||||
|
||||
'@folo-services/exceptions@0.1.16':
|
||||
resolution: {integrity: sha512-ThmLLe1Dv2CFsDIgcuY6kJhYJnjiQ7UEsewaqc25MkuImDabhzWnnad+3zRnJGLpveaMeEZ69uqUsTbKC+fR7g==}
|
||||
|
||||
|
|
@ -19018,10 +19018,10 @@ snapshots:
|
|||
- sql.js
|
||||
- sqlite3
|
||||
|
||||
'@folo-services/ai-tools@0.2.31(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)':
|
||||
'@folo-services/ai-tools@0.2.33(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai': 2.0.22(zod@3.25.76)
|
||||
'@folo-services/drizzle': 0.1.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
|
||||
'@folo-services/drizzle': 0.1.24(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
|
||||
ai: 5.0.26(zod@3.25.76)
|
||||
drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
|
||||
zod: 3.25.76
|
||||
|
|
@ -19061,7 +19061,7 @@ snapshots:
|
|||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@folo-services/drizzle@0.1.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)':
|
||||
'@folo-services/drizzle@0.1.23(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)':
|
||||
dependencies:
|
||||
'@folo-services/exceptions': 0.1.16
|
||||
drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
|
||||
|
|
@ -19100,7 +19100,7 @@ snapshots:
|
|||
- sql.js
|
||||
- sqlite3
|
||||
|
||||
'@folo-services/drizzle@0.1.23(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)':
|
||||
'@folo-services/drizzle@0.1.24(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)':
|
||||
dependencies:
|
||||
'@folo-services/exceptions': 0.1.16
|
||||
drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
|
||||
|
|
|
|||
Loading…
Reference in New Issue