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:
parent
0a687b9fd8
commit
aa386f6765
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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})`
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue