feat(ai-chat): integrate react flow to replace mermaid

- Added @xyflow/react as a dependency and integrated its flow chart functionality into the AI chat interface.
- Updated AIDisplay components to improve rendering and state management, ensuring a more responsive user experience.
- Refactored several display components for better readability and maintainability, including AIDisplayAnalyticsPart, AIDisplayEntriesPart, AIDisplayFeedsPart, and AIDisplaySubscriptionsPart.
- Introduced AIDisplayFlowPart for visualizing flow charts, enhancing the overall functionality of the chat interface.

These changes significantly improve the user experience and maintainability of the AI chat application.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-08-06 01:15:56 +08:00
parent 0a687b9fd8
commit aa386f6765
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
11 changed files with 339 additions and 202 deletions

View File

@ -49,6 +49,7 @@
"@tanstack/react-virtual": "3.13.12",
"@use-gesture/react": "10.3.1",
"@welldone-software/why-did-you-render": "10.0.1",
"@xyflow/react": "12.8.2",
"@yornaath/batshit": "0.10.1",
"ai": "5.0.2",
"camelcase-keys": "9.1.3",
@ -118,7 +119,7 @@
"@follow/models": "workspace:*",
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
"@folo-services/ai-tools": "0.2.16",
"@folo-services/ai-tools": "0.2.17",
"@types/node": "24.0.10",
"@vite-pwa/assets-generator": "1.0.0",
"fake-indexeddb": "6.0.1",

View File

@ -14,7 +14,6 @@ import {
TableRow,
} from "@follow/components/ui/table/index.js"
import dayjs from "dayjs"
import { memo } from "react"
import type { AIDisplayAnalyticsTool } from "../../store/types"
import { withDisplayStateHandler } from "./share"
@ -228,52 +227,53 @@ const OverviewAnalytics = ({ data }: { data: AnalyticsData["overviewStats"] }) =
)
}
const AIDisplayAnalyticsPartBase = memo(
({ output }: { output: NonNullable<AIDisplayAnalyticsTool["output"]> }) => {
const { analyticsData, analyticsType, timeRange, displayType, title } = output
const AIDisplayAnalyticsPartBase = ({
output,
}: {
output: NonNullable<AIDisplayAnalyticsTool["output"]>
}) => {
const { analyticsData, analyticsType, timeRange, displayType, title } = output
const renderAnalytics = () => {
switch (analyticsType) {
case "feed": {
return <FeedAnalytics data={analyticsData.feedData} />
}
case "subscription": {
return <SubscriptionAnalytics data={analyticsData.subscriptionStats} />
}
case "reading": {
return <ReadingAnalytics data={analyticsData.readingStats} />
}
case "trending": {
return <TrendingAnalytics data={analyticsData.trendingFeeds} />
}
case "overview": {
return <OverviewAnalytics data={analyticsData.overviewStats} />
}
default: {
return <div className="text-text-secondary">No analytics data available</div>
}
const renderAnalytics = () => {
switch (analyticsType) {
case "feed": {
return <FeedAnalytics data={analyticsData.feedData} />
}
case "subscription": {
return <SubscriptionAnalytics data={analyticsData.subscriptionStats} />
}
case "reading": {
return <ReadingAnalytics data={analyticsData.readingStats} />
}
case "trending": {
return <TrendingAnalytics data={analyticsData.trendingFeeds} />
}
case "overview": {
return <OverviewAnalytics data={analyticsData.overviewStats} />
}
default: {
return <div className="text-text-secondary">No analytics data available</div>
}
}
}
return (
<Card className="mx-auto mb-2 w-full max-w-4xl">
<CardHeader>
<CardTitle className="text-text flex items-center gap-2 text-xl font-semibold">
<span className="text-lg">📊</span>
<span>
{title ||
`${analyticsType.charAt(0).toUpperCase() + analyticsType.slice(1)} Analytics`}
</span>
</CardTitle>
<CardDescription>
{formatTimeRange(timeRange)} Display type: {displayType}
</CardDescription>
</CardHeader>
<CardContent className="@container">{renderAnalytics()}</CardContent>
</Card>
)
},
)
return (
<Card className="mx-auto mb-2 w-full max-w-4xl">
<CardHeader>
<CardTitle className="text-text flex items-center gap-2 text-xl font-semibold">
<span className="text-lg">📊</span>
<span>
{title || `${analyticsType.charAt(0).toUpperCase() + analyticsType.slice(1)} Analytics`}
</span>
</CardTitle>
<CardDescription>
{formatTimeRange(timeRange)} Display type: {displayType}
</CardDescription>
</CardHeader>
<CardContent className="@container">{renderAnalytics()}</CardContent>
</Card>
)
}
export const AIDisplayAnalyticsPart = withDisplayStateHandler<AIDisplayAnalyticsTool["output"]>({
title: "Analytics",

View File

@ -262,62 +262,64 @@ const GroupedEntries = ({
)
}
const AIDisplayEntriesPartBase = memo(
({ output }: { output: NonNullable<AIDisplayEntriesTool["output"]> }) => {
const {
entries,
displayType = "grid",
showSummary = true,
showMetadata = true,
title,
groupBy = "none",
} = output
const AIDisplayEntriesPartBase = ({
output,
}: {
output: NonNullable<AIDisplayEntriesTool["output"]>
}) => {
const {
entries,
displayType = "grid",
showSummary = true,
showMetadata = true,
title,
groupBy = "none",
} = output
// Calculate statistics
const totalEntries = entries.length
const feedsCount = new Set(entries.map((e) => e.entry.feedId)).size
const authorsCount = new Set(entries.map((e) => e.entry.author).filter(Boolean)).size
const categoriesCount = new Set(entries.flatMap((e) => e.entry.categories || [])).size
// Calculate statistics
const totalEntries = entries.length
const feedsCount = new Set(entries.map((e) => e.entry.feedId)).size
const authorsCount = new Set(entries.map((e) => e.entry.author).filter(Boolean)).size
const categoriesCount = new Set(entries.flatMap((e) => e.entry.categories || [])).size
const renderEntries = () => {
if (groupBy !== "none") {
return (
<GroupedEntries
data={entries}
groupBy={groupBy}
displayType={displayType}
showSummary={showSummary}
showMetadata={showMetadata}
/>
)
}
if (displayType === "timeline") {
return <TimelineView data={entries} showSummary={showSummary} showMetadata={showMetadata} />
}
return <EntriesGrid data={entries} showSummary={showSummary} showMetadata={showMetadata} />
const renderEntries = () => {
if (groupBy !== "none") {
return (
<GroupedEntries
data={entries}
groupBy={groupBy}
displayType={displayType}
showSummary={showSummary}
showMetadata={showMetadata}
/>
)
}
return (
<DisplayCardWrapper
title={title || "Entries"}
emoji="📰"
description={`${formatDisplayType(displayType)}${formatGroupBy(groupBy)}${totalEntries} entries`}
>
{/* Statistics Overview */}
<div className="@[600px]:grid-cols-4 @[400px]:grid-cols-2 grid grid-cols-1 gap-4">
<StatCard title="Total Entries" value={totalEntries} emoji="📄" />
<StatCard title="Feeds" value={feedsCount} emoji="📡" />
<StatCard title="Authors" value={authorsCount} emoji="✍️" />
<StatCard title="Categories" value={categoriesCount} emoji="🏷️" />
</div>
if (displayType === "timeline") {
return <TimelineView data={entries} showSummary={showSummary} showMetadata={showMetadata} />
}
return <EntriesGrid data={entries} showSummary={showSummary} showMetadata={showMetadata} />
}
{/* Entries Display */}
{renderEntries()}
</DisplayCardWrapper>
)
},
)
return (
<DisplayCardWrapper
title={title || "Entries"}
emoji="📰"
description={`${formatDisplayType(displayType)}${formatGroupBy(groupBy)}${totalEntries} entries`}
>
{/* Statistics Overview */}
<div className="@[600px]:grid-cols-4 @[400px]:grid-cols-2 grid grid-cols-1 gap-4">
<StatCard title="Total Entries" value={totalEntries} emoji="📄" />
<StatCard title="Feeds" value={feedsCount} emoji="📡" />
<StatCard title="Authors" value={authorsCount} emoji="✍️" />
<StatCard title="Categories" value={categoriesCount} emoji="🏷️" />
</div>
{/* Entries Display */}
{renderEntries()}
</DisplayCardWrapper>
)
}
export const AIDisplayEntriesPart = withDisplayStateHandler<AIDisplayEntriesTool["output"]>({
title: "Entries",

View File

@ -6,7 +6,6 @@ import {
CardTitle,
} from "@follow/components/ui/card/index.js"
import dayjs from "dayjs"
import { memo } from "react"
import { FeedIcon } from "~/modules/feed/feed-icon"
@ -77,52 +76,50 @@ const FeedsGrid = ({ data, showAnalytics }: { data: FeedData; showAnalytics: boo
)
}
const AIDisplayFeedsPartBase = memo(
({ output }: { output: NonNullable<AIDisplayFeedsTool["output"]> }) => {
const { feeds, showAnalytics = true, title } = output
const AIDisplayFeedsPartBase = ({
output,
}: {
output: NonNullable<AIDisplayFeedsTool["output"]>
}) => {
const { feeds, showAnalytics = true, title } = output
// Calculate statistics
const totalFeeds = feeds.length
const activeFeeds = feeds.filter((f) => !f.feed.errorMessage).length
const errorFeeds = feeds.filter((f) => f.feed.errorMessage).length
const totalSubscriptions = feeds.reduce(
(acc, f) => acc + (f.analytics?.subscriptionCount || 0),
0,
)
const totalViews = feeds.reduce((acc, f) => acc + (f.analytics?.view || 0), 0)
// Calculate statistics
const totalFeeds = feeds.length
const activeFeeds = feeds.filter((f) => !f.feed.errorMessage).length
const errorFeeds = feeds.filter((f) => f.feed.errorMessage).length
const totalSubscriptions = feeds.reduce(
(acc, f) => acc + (f.analytics?.subscriptionCount || 0),
0,
)
const totalViews = feeds.reduce((acc, f) => acc + (f.analytics?.view || 0), 0)
return (
<DisplayCardWrapper
title={title || "RSS Feeds"}
emoji="📡"
description={`${totalFeeds} feeds`}
>
{/* Statistics Overview */}
<div className="@[600px]:grid-cols-4 grid grid-cols-2 gap-4 md:grid-cols-4">
<StatCard title="Total Feeds" value={totalFeeds} emoji="📊" />
<StatCard
title="Active Feeds"
value={activeFeeds}
description={`${errorFeeds} with errors`}
emoji="🟢"
/>
{showAnalytics && (
<>
<StatCard
title="Total Subscribers"
value={totalSubscriptions.toLocaleString()}
emoji="👥"
/>
<StatCard title="Total Views" value={totalViews.toLocaleString()} emoji="👀" />
</>
)}
</div>
return (
<DisplayCardWrapper title={title || "RSS Feeds"} emoji="📡" description={`${totalFeeds} feeds`}>
{/* Statistics Overview */}
<div className="@[600px]:grid-cols-4 grid grid-cols-2 gap-4 md:grid-cols-4">
<StatCard title="Total Feeds" value={totalFeeds} emoji="📊" />
<StatCard
title="Active Feeds"
value={activeFeeds}
description={`${errorFeeds} with errors`}
emoji="🟢"
/>
{showAnalytics && (
<>
<StatCard
title="Total Subscribers"
value={totalSubscriptions.toLocaleString()}
emoji="👥"
/>
<StatCard title="Total Views" value={totalViews.toLocaleString()} emoji="👀" />
</>
)}
</div>
<FeedsGrid data={feeds} showAnalytics={showAnalytics} />
</DisplayCardWrapper>
)
},
)
<FeedsGrid data={feeds} showAnalytics={showAnalytics} />
</DisplayCardWrapper>
)
}
export const AIDisplayFeedsPart = withDisplayStateHandler<AIDisplayFeedsTool["output"]>({
title: "Feeds",

View File

@ -0,0 +1,41 @@
import "@xyflow/react/dist/style.css"
import { useIsDark } from "@follow/hooks"
import { Background, ReactFlow } from "@xyflow/react"
import type { AIDisplayFlowTool } from "../../store/types"
import { toolMemo } from "./share"
export const AIDisplayFlowPart = toolMemo(
({ part, loadingElement }: { part: AIDisplayFlowTool; loadingElement: React.ReactNode }) => {
if (!part.input) return loadingElement
if (part.state === "input-streaming") {
return loadingElement
}
const { nodes, edges } = part.input.flowChart as any
const colorMode = useIsDark() ? "dark" : "light"
return (
<div className="my-2 aspect-[4/3] w-full overflow-hidden rounded-md">
<ReactFlow
colorMode={colorMode}
nodes={nodes}
edges={edges}
fitView
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
elementsSelectable={false}
panOnDrag={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
preventScrolling={false}
>
<Background />
</ReactFlow>
</div>
)
},
)

View File

@ -6,7 +6,6 @@ import {
CardTitle,
} from "@follow/components/ui/card/index.js"
import dayjs from "dayjs"
import { memo } from "react"
import { FeedIcon } from "~/modules/feed/feed-icon"
@ -167,41 +166,35 @@ const GroupedSubscriptions = ({
)
}
const AIDisplaySubscriptionsPartBase = memo(
({ output }: { output: NonNullable<AIDisplaySubscriptionsTool["output"]> }) => {
const {
subscriptions,
displayType = "list",
showAnalytics = true,
showCategories = true,
title,
groupBy = "none",
filterBy = "all",
} = output
const AIDisplaySubscriptionsPartBase = ({
output,
}: {
output: NonNullable<AIDisplaySubscriptionsTool["output"]>
}) => {
const {
subscriptions,
displayType = "list",
showAnalytics = true,
showCategories = true,
title,
groupBy = "none",
filterBy = "all",
} = output
// Calculate statistics
const totalSubscriptions = subscriptions.length
const categoriesCount = new Set(
subscriptions.map((s) => s.subscription?.category).filter(Boolean),
).size
const activeSubscriptions = subscriptions.filter((s) => !s.feed?.errorMessage).length
const totalViews = subscriptions.reduce((acc, s) => acc + (s.subscription?.view || 0), 0)
const renderSubscriptions = () => {
if (groupBy !== "none") {
return (
<GroupedSubscriptions
data={subscriptions}
groupBy={groupBy}
showAnalytics={showAnalytics}
showCategories={showCategories}
/>
)
}
// Calculate statistics
const totalSubscriptions = subscriptions.length
const categoriesCount = new Set(
subscriptions.map((s) => s.subscription?.category).filter(Boolean),
).size
const activeSubscriptions = subscriptions.filter((s) => !s.feed?.errorMessage).length
const totalViews = subscriptions.reduce((acc, s) => acc + (s.subscription?.view || 0), 0)
const renderSubscriptions = () => {
if (groupBy !== "none") {
return (
<SubscriptionsGrid
<GroupedSubscriptions
data={subscriptions}
groupBy={groupBy}
showAnalytics={showAnalytics}
showCategories={showCategories}
/>
@ -209,30 +202,38 @@ const AIDisplaySubscriptionsPartBase = memo(
}
return (
<DisplayCardWrapper
title={title || "My Subscriptions"}
emoji="📋"
description={`${formatDisplayType(displayType)}${formatFilterBy(filterBy)}${formatGroupBy(groupBy)}`}
>
{/* Statistics Overview */}
<div className="@[700px]:grid-cols-4 grid grid-cols-2 gap-4">
<StatCard title="Total Subscriptions" value={totalSubscriptions} emoji="📊" />
<StatCard
title="Active Feeds"
value={activeSubscriptions}
description={`${totalSubscriptions - activeSubscriptions} inactive`}
emoji="🟢"
/>
{showCategories && <StatCard title="Categories" value={categoriesCount} emoji="🏷️" />}
<StatCard title="Total Views" value={totalViews.toLocaleString()} emoji="👀" />
</div>
{/* Subscriptions Display */}
{renderSubscriptions()}
</DisplayCardWrapper>
<SubscriptionsGrid
data={subscriptions}
showAnalytics={showAnalytics}
showCategories={showCategories}
/>
)
},
)
}
return (
<DisplayCardWrapper
title={title || "My Subscriptions"}
emoji="📋"
description={`${formatDisplayType(displayType)}${formatFilterBy(filterBy)}${formatGroupBy(groupBy)}`}
>
{/* Statistics Overview */}
<div className="@[700px]:grid-cols-4 grid grid-cols-2 gap-4">
<StatCard title="Total Subscriptions" value={totalSubscriptions} emoji="📊" />
<StatCard
title="Active Feeds"
value={activeSubscriptions}
description={`${totalSubscriptions - activeSubscriptions} inactive`}
emoji="🟢"
/>
{showCategories && <StatCard title="Categories" value={categoriesCount} emoji="🏷️" />}
<StatCard title="Total Views" value={totalViews.toLocaleString()} emoji="👀" />
</div>
{/* Subscriptions Display */}
{renderSubscriptions()}
</DisplayCardWrapper>
)
}
export const AIDisplaySubscriptionsPart = withDisplayStateHandler<
AIDisplaySubscriptionsTool["output"]

View File

@ -33,7 +33,7 @@ export function withDisplayStateHandler<T>(config: {
return function <P extends { part: { state: string; output?: T } }>(
WrappedComponent: ComponentType<P & { output: NonNullable<T> }>,
): ComponentType<P> {
const WithDisplayStateHandler = (props: P) => {
const WithDisplayStateHandler = toolMemo((props: P) => {
const { part } = props
// Handle error state
@ -60,7 +60,7 @@ export function withDisplayStateHandler<T>(config: {
// Render the wrapped component with the validated output
return <WrappedComponent {...props} output={part.output as NonNullable<T>} />
}
})
WithDisplayStateHandler.displayName = `withDisplayStateHandler(${WrappedComponent.displayName || WrappedComponent.name})`

View File

@ -193,7 +193,7 @@ const ChatInterfaceContent = () => {
className={clsx(
"absolute mx-auto duration-200 ease-in-out",
hasMessages && "inset-x-0 bottom-0 max-w-4xl px-6 pb-6",
!hasMessages && "inset-x-0 bottom-1/2 max-w-3xl translate-y-full px-6 pb-6",
!hasMessages && "inset-x-0 bottom-1/2 max-w-3xl translate-y-[calc(100%+2rem)] px-6 pb-6",
)}
>
{error && <CollapsibleError error={error} />}

View File

@ -1,3 +1,5 @@
import "@xyflow/react/dist/style.css"
import type { ToolUIPart } from "ai"
import type { SerializedEditorState } from "lexical"
import { m } from "motion/react"
@ -8,6 +10,7 @@ import type {
AIDisplayAnalyticsTool,
AIDisplayEntriesTool,
AIDisplayFeedsTool,
AIDisplayFlowTool,
AIDisplaySubscriptionsTool,
BizUIMessage,
} from "~/modules/ai-chat/store/types"
@ -18,11 +21,16 @@ import {
AIDisplayFeedsPart,
AIDisplaySubscriptionsPart,
} from "../displays"
// import { AIDisplayFlowPart } from "../displays/AIDisplayFlowPart"
import { AIDataBlockPart } from "./AIDataBlockPart"
import { AIMarkdownMessage, AIMarkdownStreamingMessage } from "./AIMarkdownMessage"
import { AIRichTextMessage } from "./AIRichTextMessage"
import { ToolInvocationComponent } from "./ToolInvocationComponent"
const LazyAIDisplayFlowPart = React.lazy(() =>
import("../displays/AIDisplayFlowPart").then((mod) => ({ default: mod.AIDisplayFlowPart })),
)
interface MessagePartsProps {
message: BizUIMessage
}
@ -98,6 +106,29 @@ export const AIMessageParts: React.FC<MessagePartsProps> = React.memo(({ message
return <AIDisplayFeedsPart key={partKey} part={part as AIDisplayFeedsTool} />
}
case "tool-displayFlowChart": {
const loadingElement = (
<div className="my-2 flex aspect-[4/3] w-full items-center justify-center">
<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>
)
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")) {

View File

@ -18,5 +18,6 @@ export type AIDisplayAnalyticsTool = ToolWithState<BizUITools["displayAnalytics"
export type AIDisplayFeedsTool = ToolWithState<BizUITools["displayFeeds"]>
export type AIDisplayEntriesTool = ToolWithState<BizUITools["displayEntries"]>
export type AIDisplaySubscriptionsTool = ToolWithState<BizUITools["displaySubscriptions"]>
export type AIDisplayFlowTool = ToolWithState<BizUITools["displayFlowChart"]>
export { type BizUIMessage, type BizUIMetadata, type BizUITools } from "@folo-services/ai-tools"

View File

@ -559,6 +559,9 @@ importers:
'@welldone-software/why-did-you-render':
specifier: 10.0.1
version: 10.0.1(react@19.0.0)
'@xyflow/react':
specifier: 12.8.2
version: 12.8.2(@types/react@19.1.8)(immer@10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@yornaath/batshit':
specifier: 0.10.1
version: 0.10.1
@ -762,8 +765,8 @@ importers:
specifier: workspace:*
version: link:../../../../packages/internal/utils
'@folo-services/ai-tools':
specifier: 0.2.16
version: 0.2.16(@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.17
version: 0.2.17(@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
@ -4063,8 +4066,8 @@ packages:
'@follow-app/client-sdk@0.3.26':
resolution: {integrity: sha512-rIQq8fgxEsqu0o5DjY0Q8tTpK75ZLqfo1laW5KQVPL3ME8omljFTLXyX6khe7NXCx+1yHJGzJQ5S89qZGeb20g==}
'@folo-services/ai-tools@0.2.16':
resolution: {integrity: sha512-ZMV8dMxGGkbm5P3X4+0mguC1o0s+UbLyczsbHUawM0cR7+TWsnC7JYeZ7mifxZKz3nucsfg7oV89A++6p/Qeiw==}
'@folo-services/ai-tools@0.2.17':
resolution: {integrity: sha512-TQDWY4PpLtXqw+YkvmEcCg6JnbHDolbh2EJt6YzRXbfSm3qs+bonY9sPkqjG8PN2hqO7kYEmy8JBXGJdpGGwEw==}
'@folo-services/constants@0.1.16':
resolution: {integrity: sha512-zTpp0izvwK+t1uZurU0gh1yt5M253tfHdFOzxPi7mdUDdsI/cfXtwrBAbRYz1ne27RjRTP04KpzW2U5QOaDzhA==}
@ -7010,6 +7013,15 @@ packages:
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'}
'@xyflow/react@12.8.2':
resolution: {integrity: sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==}
peerDependencies:
react: 19.0.0
react-dom: 19.0.0
'@xyflow/system@0.0.66':
resolution: {integrity: sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==}
'@yornaath/batshit-devtools@1.7.1':
resolution: {integrity: sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==}
@ -7858,6 +7870,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
clean-css@5.3.3:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
@ -16082,6 +16097,21 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: 19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zustand@5.0.6:
resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==}
engines: {node: '>=12.20.0'}
@ -19255,7 +19285,7 @@ snapshots:
- sql.js
- sqlite3
'@folo-services/ai-tools@0.2.16(@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.17(@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.2(zod@3.25.76)
'@folo-services/drizzle': 0.1.11(@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)
@ -22913,6 +22943,29 @@ snapshots:
'@xmldom/xmldom@0.8.10': {}
'@xyflow/react@12.8.2(@types/react@19.1.8)(immer@10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@xyflow/system': 0.0.66
classcat: 5.0.5
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
zustand: 4.5.7(@types/react@19.1.8)(immer@10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c))(react@19.0.0)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.66':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
'@yornaath/batshit-devtools@1.7.1': {}
'@yornaath/batshit@0.10.1':
@ -23986,6 +24039,8 @@ snapshots:
dependencies:
clsx: 2.1.1
classcat@5.0.5: {}
clean-css@5.3.3:
dependencies:
source-map: 0.6.1
@ -33543,6 +33598,14 @@ snapshots:
zod@3.25.76: {}
zustand@4.5.7(@types/react@19.1.8)(immer@10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c))(react@19.0.0):
dependencies:
use-sync-external-store: 1.5.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.1.8
immer: 10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c)
react: 19.0.0
zustand@5.0.6(@types/react@19.1.8)(immer@10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c))(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)):
optionalDependencies:
'@types/react': 19.1.8