From 70bf3fad2bf20254a7e13414d8e65f3cd6eecfeb Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 27 Aug 2025 23:08:32 +0800 Subject: [PATCH] feat(ai): add ai context usage info - Upgraded the version of `@folo-services/ai-tools` from 0.2.20 to 0.2.31 in the package.json file. - Introduced a new `TokenUsagePill` component to display token usage information in the AI chat message. - Updated various components to utilize the new `TokenUsagePill` for better visibility of token metrics. - Refactored type definitions in the chat store and related components to improve type safety and clarity. These changes aim to enhance the user experience by providing clearer insights into token usage and improving the overall structure of the AI chat components. Signed-off-by: Innei --- apps/desktop/layer/renderer/package.json | 2 +- .../displays/AIDisplayFeedsPart.tsx | 2 +- .../components/layouts/ChatInterface.tsx | 3 +- .../components/message/AIChatMessage.tsx | 15 +- .../components/message/AIDataBlockPart.tsx | 2 +- .../components/message/TokenUsagePill.tsx | 114 ++++++++++++ .../components/message/UserChatMessage.tsx | 5 +- .../src/modules/ai-chat/services/index.ts | 4 +- .../ai-chat/store/chat-core/chat-actions.ts | 6 +- .../ai-chat/store/chat-core/chat-instance.ts | 6 +- .../ai-chat/store/chat-core/chat-state.ts | 55 ++++-- .../store/event-system/event-emitter.ts | 6 +- .../ai-chat/store/slices/chat.slice.ts | 3 +- .../src/modules/ai-chat/store/types.ts | 4 +- .../tabs/ai/usage/UsageAnalysisSection.tsx | 9 +- .../usage/components/DetailedUsageModal.tsx | 21 ++- .../ai/usage/components/EfficiencyTab.tsx | 17 +- .../tabs/ai/usage/components/OverviewTab.tsx | 7 +- .../tabs/ai/usage/components/PatternsTab.tsx | 5 +- .../modules/settings/tabs/ai/usage/utils.ts | 5 + pnpm-lock.yaml | 168 ++++++++++-------- 21 files changed, 322 insertions(+), 137 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx diff --git a/apps/desktop/layer/renderer/package.json b/apps/desktop/layer/renderer/package.json index 5f91fcc9b..d7139ef74 100644 --- a/apps/desktop/layer/renderer/package.json +++ b/apps/desktop/layer/renderer/package.json @@ -119,7 +119,7 @@ "@follow/models": "workspace:*", "@follow/types": "workspace:*", "@follow/utils": "workspace:*", - "@folo-services/ai-tools": "0.2.20", + "@folo-services/ai-tools": "0.2.31", "@types/node": "24.0.10", "@vite-pwa/assets-generator": "1.0.0", "fake-indexeddb": "6.0.1", diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx index 60cb7c1ca..279c007f5 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx @@ -96,7 +96,7 @@ const AIDisplayFeedsPartBase = ({ return ( {/* Statistics Overview */} -
+
{ (message: string | EditorState, editor: LexicalEditor | null) => { resetScrollState() - const blocks = [] as any[] + const blocks = [] as AIChatContextBlock[] for (const block of blockActions.getBlocks()) { if (block.type === "fileAttachment" && block.attachment.serverUrl) { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx index 2912dfeee..c8e84a2dd 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx @@ -7,12 +7,13 @@ import * as React from "react" import { toast } from "sonner" import { copyToClipboard } from "~/lib/clipboard" -import type { BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types" +import type { BizUIMessage, BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types" import { MentionPlugin } from "../../editor" import type { RichTextPart } from "../../types/ChatSession" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" import { AIMessageParts } from "./AIMessageParts" +import { TokenUsagePill } from "./TokenUsagePill" export interface ChatMessage { id: string @@ -22,7 +23,7 @@ export interface ChatMessage { } interface AIChatMessageProps { - message: UIMessage + message: BizUIMessage } // Utility function for converting message to markdown @@ -96,6 +97,16 @@ export const AIChatMessage: React.FC = React.memo(({ message Copy + {message.metadata && ( + + + + )}
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx index 4ee230941..a9710040f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx @@ -23,7 +23,7 @@ export const AIDataBlockPart: React.FC = React.memo(({ blo
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx new file mode 100644 index 000000000..9ff9996cd --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx @@ -0,0 +1,114 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js" +import type { BizUIMetadata } from "@folo-services/ai-tools" +import * as React from "react" + +import { formatTokenCountString } from "~/modules/settings/tabs/ai/usage/utils" + +interface TokenUsagePillProps { + metadata: BizUIMetadata | undefined + className?: string + children: React.ReactNode +} + +const formatDuration = (ms: number): string => { + if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s` + return `${ms}ms` +} + +export const TokenUsagePill: React.FC = ({ metadata, children }) => { + if (!metadata) return null + + const hasReasoningTokens = metadata.reasoningTokens != null && metadata.reasoningTokens > 0 + const hasCachedInputTokens = metadata.cachedInputTokens != null && metadata.cachedInputTokens > 0 + const hasBillingMultiplier = + metadata.billingMultiplier != null && metadata.billingMultiplier !== 1 + const hasDuration = metadata.duration != null + + return ( + + {children} + +
+
Model Info
+
+ {metadata.modelUsed ?? "Unknown"} +
+
+
+
Token Usage
+
+ {metadata.totalTokens != null && ( +
+ Total: + + {formatTokenCountString(metadata.totalTokens)} + +
+ )} + {metadata.billedTokens != null && ( +
+ Billed: + + {formatTokenCountString(metadata.billedTokens)} + +
+ )} + {metadata.contextTokens != null && ( +
+ Context: + + {formatTokenCountString(metadata.contextTokens)} + +
+ )} + {metadata.outputTokens != null && ( +
+ Output: + + {formatTokenCountString(metadata.outputTokens)} + +
+ )} + {hasReasoningTokens && ( +
+ Reasoning: + + {formatTokenCountString(metadata.reasoningTokens!)} + +
+ )} + {hasCachedInputTokens && ( +
+ Cached: + + {formatTokenCountString(metadata.cachedInputTokens!)} + +
+ )} +
+ {(hasDuration || hasBillingMultiplier) && ( + <> +
+
+ {hasDuration && ( +
+ Duration: + + {formatDuration(metadata.duration!)} + +
+ )} + {hasBillingMultiplier && ( +
+ Multiplier: + {metadata.billingMultiplier!}ร— +
+ )} +
+ + )} +
+
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx index 216bd1b16..45dd6e6e6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx @@ -1,13 +1,12 @@ import { Spring } from "@follow/components/constants/spring.js" import { stopPropagation, thenable } from "@follow/utils" -import type { UIDataTypes, UIMessage } from "ai" import type { LexicalEditor, SerializedEditorState } from "lexical" import { AnimatePresence, m } from "motion/react" import * as React from "react" import { useEditingMessageId, useSetEditingMessageId } from "~/modules/ai-chat/atoms/session" import { useChatActions } from "~/modules/ai-chat/store/hooks" -import type { AIChatContextBlock, BizUIMetadata, BizUITools } from "~/modules/ai-chat/store/types" +import type { AIChatContextBlock, BizUIMessage } from "~/modules/ai-chat/store/types" import type { RichTextPart } from "../../types/ChatSession" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" @@ -16,7 +15,7 @@ import { EditableMessage } from "./EditableMessage" import { UserMessageParts } from "./UserMessageParts" interface UserChatMessageProps { - message: UIMessage + message: BizUIMessage } export const UserChatMessage: React.FC = React.memo(({ message }) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts index c7eb2414a..e9e8c68d6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts @@ -157,8 +157,6 @@ class AIPersistServiceStatic { messages .filter((message) => message.parts.length > 0) .map((message) => { - const convertedParts = message.parts as any[] - return { id: message.id, chatId, @@ -169,7 +167,7 @@ class AIPersistServiceStatic { finishedAt: message.metadata?.finishTime ? new Date(message.metadata.finishTime) : undefined, - messageParts: convertedParts, + messageParts: message.parts, metadata: message.metadata, } as typeof aiChatMessagesTable.$inferInsert }), diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts index c2f67fe79..a7ba5b9c3 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts @@ -12,7 +12,7 @@ import type { ChatSlice } from "./types" export class ChatSliceActions { constructor( private params: Parameters>, - private chatInstance: ZustandChat, + private chatInstance: ZustandChat, ) { return autoBindThis(this) } @@ -182,7 +182,7 @@ export class ChatSliceActions { this.chatInstance.destroy() // Create new chat instance - const newChatInstance = new ZustandChat( + const newChatInstance = new ZustandChat( { id: newChatId, messages: [], @@ -221,7 +221,7 @@ export class ChatSliceActions { this.chatInstance.destroy() // Create new chat instance with loaded messages - const newChatInstance = new ZustandChat( + const newChatInstance = new ZustandChat( { id: chatId, messages, diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts index 84183105c..6b9d9a086 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts @@ -6,12 +6,12 @@ import { ZustandChatState } from "./chat-state" import type { ChatSlice } from "./types" // Custom Chat class that uses Zustand-integrated state -export class ZustandChat extends AbstractChat { - override state: ZustandChatState +export class ZustandChat extends AbstractChat { + override state: ZustandChatState #unsubscribeFns: (() => void)[] = [] constructor( - { messages, ...init }: ChatInit, + { messages, ...init }: ChatInit, updateZustandState: (updater: (state: ChatSlice) => ChatSlice) => void, ) { const state = new ZustandChatState(messages, updateZustandState, init.id || "") diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts index 53a69c420..41b980fd2 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts @@ -1,5 +1,7 @@ +/* eslint-disable unicorn/no-for-loop */ import type { ChatState, ChatStatus } from "ai" import { throttle } from "es-toolkit/compat" +import { produce } from "immer" import { AIPersistService } from "../../services" import { ChatStateEventEmitter } from "../event-system/event-emitter" @@ -7,14 +9,14 @@ import type { BizUIMessage } from "../types" import type { ChatSlice } from "./types" // Zustand Chat State that implements AI SDK ChatState interface -export class ZustandChatState implements ChatState { - #messages: UI_MESSAGE[] +export class ZustandChatState implements ChatState { + #messages: BizUIMessage[] #status: ChatStatus = "ready" #error: Error | undefined = undefined - #eventEmitter = new ChatStateEventEmitter() + #eventEmitter = new ChatStateEventEmitter() constructor( - initialMessages: UI_MESSAGE[] = [], + initialMessages: BizUIMessage[] = [], private updateZustandState: (updater: (state: ChatSlice) => ChatSlice) => void, private chatId: string, ) { @@ -25,10 +27,36 @@ export class ZustandChatState implements ChatSt #setupEventHandlers(): void { // Setup event handlers for automatic Zustand synchronization this.#eventEmitter.on("messages", ({ messages }) => { - this.updateZustandState((state) => ({ - ...state, - messages: [...messages], - })) + this.updateZustandState( + produce((state) => { + const stateMessages = state.messages + for (let i = 0; i < messages.length; i++) { + const message = messages[i]! + if (!stateMessages[i]) { + stateMessages[i] = structuredClone(message) as any + } else { + const stateMessage = stateMessages[i]! + stateMessage.id = message.id + + for (let j = 0; j < message.parts.length; j++) { + const statePart = stateMessage.parts[j] || {} + const messagePart = message.parts[j]! + + Object.assign(statePart, messagePart) + stateMessage.parts[j] = statePart as any + } + + stateMessage.parts.length = message.parts.length + + stateMessage.role = message.role + + stateMessage.metadata = stateMessage.metadata ?? {} + Object.assign(stateMessage.metadata, message.metadata) + } + } + stateMessages.length = messages.length + }), + ) }) this.#eventEmitter.on("status", ({ status }) => { @@ -69,19 +97,20 @@ export class ZustandChatState implements ChatSt this.#eventEmitter.emit("error", { error: newError }) } - get messages(): UI_MESSAGE[] { + get messages(): BizUIMessage[] { return this.#messages } - set messages(newMessages: UI_MESSAGE[]) { + set messages(newMessages: BizUIMessage[]) { this.#messages = [...newMessages] + this.#eventEmitter.emit("messages", { messages: this.#messages }) // Auto-persist messages when they change this.#persistMessages() } - pushMessage = (message: UI_MESSAGE) => { + pushMessage = (message: BizUIMessage) => { this.messages = this.#messages.concat(message) } @@ -91,7 +120,7 @@ export class ZustandChatState implements ChatSt this.messages = this.#messages.slice(0, -1) } - replaceMessage = (index: number, message: UI_MESSAGE) => { + replaceMessage = (index: number, message: BizUIMessage) => { if (index < 0 || index >= this.#messages.length) return this.messages = [ @@ -123,7 +152,7 @@ export class ZustandChatState implements ChatSt } // Internal event subscription with payload access - onMessagesChange = (listener: (messages: UI_MESSAGE[]) => void): (() => void) => { + onMessagesChange = (listener: (messages: BizUIMessage[]) => void): (() => void) => { return this.#eventEmitter.on("messages", ({ messages }) => listener(messages)) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts index c35d76a70..09f105e82 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts @@ -2,12 +2,12 @@ import type { BizUIMessage } from "../types" import type { ChatStateEvents, ChatStateEventType } from "./types" // Event emitter for AI chat state changes with typed payloads -export class ChatStateEventEmitter { +export class ChatStateEventEmitter { #listeners = new Map void>>() on( event: T, - listener: (payload: ChatStateEvents[T]) => void, + listener: (payload: ChatStateEvents[T]) => void, ): () => void { if (!this.#listeners.has(event)) { this.#listeners.set(event, new Set()) @@ -20,7 +20,7 @@ export class ChatStateEventEmitter { } } - emit(event: T, payload: ChatStateEvents[T]): void { + emit(event: T, payload: ChatStateEvents[T]): void { this.#listeners.get(event)?.forEach((listener) => { try { listener(payload) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts index 800b924b5..6bebd46c7 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts @@ -7,14 +7,13 @@ import { ChatSliceActions } from "../chat-core/chat-actions" import { ZustandChat } from "../chat-core/chat-instance" import type { ChatSlice } from "../chat-core/types" import { createChatTransport } from "../transport" -import type { BizUIMessage } from "../types" export const createChatSlice: StateCreator = (...params) => { const [set, get] = params const chatId = nanoid() // Create chat instance with Zustand integration - const chatInstance = new ZustandChat( + const chatInstance = new ZustandChat( { id: chatId, messages: [], diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index 79567486f..4df1bce26 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -5,9 +5,9 @@ export interface FileAttachment { name: string type: string size: number - dataUrl: string + dataUrl?: string previewUrl?: string - uploadStatus: "processing" | "uploading" | "completed" | "error" + uploadStatus?: "processing" | "uploading" | "completed" | "error" serverUrl?: string errorMessage?: string /** Upload progress percentage (0-100) */ diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx index e16b3d99a..e5fceeec6 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/UsageAnalysisSection.tsx @@ -5,7 +5,7 @@ import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useAIConfiguration } from "~/modules/ai-chat/hooks/useAIConfiguration" import { DetailedUsageModal, UsageProgressRing, UsageWarningBanner } from "./components" -import { formatTimeRemaining, formatTokenCount } from "./utils" +import { formatTimeRemaining, formatTokenCountString } from "./utils" export const UsageAnalysisSection = () => { const { t } = useTranslation("ai") @@ -57,8 +57,7 @@ export const UsageAnalysisSection = () => {
- {formatTokenCount(rateLimit.remainingTokens).value} - {formatTokenCount(rateLimit.remainingTokens).unit} + {formatTokenCountString(rateLimit.remainingTokens)} {t("usage_analysis.tokens_remaining")} @@ -66,9 +65,7 @@ export const UsageAnalysisSection = () => {
- {formatTokenCount(usage.used).value} - {formatTokenCount(usage.used).unit} / {formatTokenCount(usage.total).value} - {formatTokenCount(usage.total).unit} used + {formatTokenCountString(usage.used)} / {formatTokenCountString(usage.total)} used
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx index b9074d623..95eff41c9 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx @@ -78,6 +78,11 @@ export const DetailedUsageModal = () => { }) const maxHourCount = Math.max(1, ...hourBuckets) + const formattedUsageTokens = formatTokenCount(usage.used) + const formattedRemainingTokens = formatTokenCount(rateLimit.remainingTokens) + const formattedTotalTokens = formatTokenCount(usage.total) + const formattedAvgDaily = formatTokenCount(avgDaily) + return (

@@ -106,15 +111,15 @@ export const DetailedUsageModal = () => {

@@ -122,8 +127,8 @@ export const DetailedUsageModal = () => {
@@ -139,8 +144,8 @@ export const DetailedUsageModal = () => {
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx index 9fdc2b312..7e3456f4b 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/ import type { ModelPattern } from "@follow-app/client-sdk" import { useTranslation } from "react-i18next" -import { formatTokenCount } from "../utils" +import { formatTokenCount, formatTokenCountString } from "../utils" import { BarList } from "./charts" interface EfficiencyTabProps { @@ -22,12 +22,15 @@ export const EfficiencyTab = ({ byModel }: EfficiencyTabProps) => { {byModel?.length > 0 ? ( ({ - label: m.model ?? "unknown", - value: m.avgEfficiency || 0, - right: `${formatTokenCount(m.totalTokens ?? 0).value}${formatTokenCount(m.totalTokens ?? 0).unit}`, - }))} - format={(v) => `${formatTokenCount(v).value}${formatTokenCount(v).unit}`} + data={byModel.map((m) => { + const formatted = formatTokenCount(m.totalTokens ?? 0) + return { + label: m.model ?? "unknown", + value: m.avgEfficiency || 0, + right: `${formatted.value}${formatted.unit}`, + } + })} + format={(v) => formatTokenCountString(v)} /> ) : (
diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx index c38078484..549298be3 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/ import type { DailyPattern } from "@follow-app/client-sdk" import { useTranslation } from "react-i18next" -import { formatTokenCount } from "../utils" +import { formatTokenCountString } from "../utils" import { Sparkline } from "./charts" interface OverviewTabProps { @@ -33,10 +33,7 @@ export const OverviewTab = ({ dailyTotals, peakDay }: OverviewTabProps) => { {peakDay?.date ? ( {t("analytics.peak", { defaultValue: "Peak" })}: - - {formatTokenCount(peakDay.totalTokens).value} - {formatTokenCount(peakDay.totalTokens).unit} - + {formatTokenCountString(peakDay.totalTokens)} {" ยท "} {new Date(peakDay.date).toLocaleDateString()} diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx index a15678085..a2ac4ef73 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/ import type { UsagePattern } from "@follow-app/client-sdk" import { useTranslation } from "react-i18next" -import { formatTokenCount } from "../utils" +import { formatTokenCountString } from "../utils" import { BarList, TinyBars } from "./charts" interface PatternsTabProps { @@ -53,7 +53,8 @@ export const PatternsTab = ({ hourBuckets, maxHourCount, byOperation }: Patterns data={byOperation.map((o) => ({ label: o.operationType ?? "unknown", value: o.percentage || 0, - right: `${formatTokenCount(o.totalTokens ?? 0).value}${formatTokenCount(o.totalTokens ?? 0).unit}`, + + right: formatTokenCountString(o.totalTokens ?? 0), }))} suffix="%" /> diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts index a3ac767df..97bb64c7c 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/utils.ts @@ -15,3 +15,8 @@ export const formatTimeRemaining = (ms: number): string => { const rem = minutes % 60 return rem ? `${hours}h ${rem}m` : `${hours}h` } + +export const formatTokenCountString = (count: number): string => { + const formatted = formatTokenCount(count) + return `${formatted.value}${formatted.unit}` +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 312f2d9b1..02c27100a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -768,8 +768,8 @@ importers: specifier: workspace:* version: link:../../../../packages/internal/utils '@folo-services/ai-tools': - specifier: 0.2.20 - version: 0.2.20(@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.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) '@types/node': specifier: 24.0.10 version: 24.0.10 @@ -1967,20 +1967,20 @@ packages: graphql: optional: true + '@ai-sdk/gateway@1.0.15': + resolution: {integrity: sha512-xySXoQ29+KbGuGfmDnABx+O6vc7Gj7qugmj1kGpn0rW0rQNn6UKUuvscKMzWyv1Uv05GyC1vqHq8ZhEOLfXscQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/gateway@1.0.2': resolution: {integrity: sha512-RCE6v6/7JMzSHmjB0R7oWT54y/VYeEv+6NYLtkQKdxN871UawFFHpL7TW7IRnn+/SHdRlr7XoWxxGdHEaGRKiA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4 - '@ai-sdk/openai@2.0.2': - resolution: {integrity: sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4 - - '@ai-sdk/provider-utils@3.0.0': - resolution: {integrity: sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==} + '@ai-sdk/openai@2.0.22': + resolution: {integrity: sha512-qjSIPL5+LNM9flcBPeR64ZWeAZdYg4XWkAK34H3FaY61dSbuIaeqFPSzmQUrxotVcphAzgfL5tuYRqRYP2ZYyg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4 @@ -1991,6 +1991,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.7': + resolution: {integrity: sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider@2.0.0': resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} @@ -4048,20 +4054,17 @@ packages: '@follow-app/client-sdk@0.3.48': resolution: {integrity: sha512-nPRnKQwzTTvPaIMOfZierMi9+TUlxc2his1FrGmkXOUbyMe6VtnChMtxHWU03Kdb2mnCJrVmOzJa0CwW6NAvmQ==} - '@folo-services/ai-tools@0.2.20': - resolution: {integrity: sha512-1AJI48H2g64JhuTruSqQKUrn4Tc353hMFdKAbohh65AxTAMN/1pIw9w17Dtjm+g5BVlZUXYKaiIvaN9tHsXLNA==} + '@folo-services/ai-tools@0.2.31': + resolution: {integrity: sha512-oqTVMBc6UCLOt/3M9GX4I+DJ8WT2U8Hlok1MN/Sruw8sQu0ZvtoDvkuiJEtssOQAGDifzHm7z8AVmOgNoGrKUg==} '@folo-services/constants@0.1.27': resolution: {integrity: sha512-ZCTgPvLiNfKiOU3E7m0x6Y6w/8PIkEHVl7C5pH+Tt36DrM81vHZjy+zPnZISgjOd3DYTJFHTOCI2ReFRx8Ir3g==} - '@folo-services/drizzle@0.1.13': - resolution: {integrity: sha512-h6tndGnTLrAmfvxK5XsXGdZaMi2GBmFZBFiBqtabC7Y05A1ZSOaWg/d9f60Ydh586LmVonz5VGrhvzTdn5ukhg==} - '@folo-services/drizzle@0.1.21': resolution: {integrity: sha512-yJAPJz05h2uio5wPOHYbzaeFhJNn5UU9S4HPLQa3L8DNE9IG9DMsNAJqFTEpXQPwEQOf2n3k8FCF8GP/SygA7w==} - '@folo-services/exceptions@0.1.11': - resolution: {integrity: sha512-OMKat8KcIxjw9pBfG3pC1kRcIs0YiuaYmMDA2MRy8AEGcki8F9u5e5QH7OKgP/LlNR7Fs2Csz1XetvcC4nB+RQ==} + '@folo-services/drizzle@0.1.22': + resolution: {integrity: sha512-n4L0bBA5AxqJn5K3k+BTzmtuZh90LAO7G6zALCo3H+BOsnMsQ1OCsQH/xFg7m6ICQ80LJVbHdUN6b4ZKUoQJ/g==} '@folo-services/exceptions@0.1.16': resolution: {integrity: sha512-ThmLLe1Dv2CFsDIgcuY6kJhYJnjiQ7UEsewaqc25MkuImDabhzWnnad+3zRnJGLpveaMeEZ69uqUsTbKC+fR7g==} @@ -7002,6 +7005,12 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@5.0.26: + resolution: {integrity: sha512-bGNtG+nYQ2U+5mzuLbxIg9WxGQJ2u5jv2gYgP8C+CJ1YI4qqIjvjOgGEZWzvNet8jiOGIlqstsht9aQefKzmBw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + ai@5.0.4: resolution: {integrity: sha512-nizyqRxFlJShCAh/xPqor9EYpuaiGqCcYqau9A2GaSL+di+Qf6pA+Hos5x1orlodmT+EciZfoC2Sq+I12OmIUg==} engines: {node: '>=18'} @@ -9428,6 +9437,10 @@ packages: resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} engines: {node: '>=20.0.0'} + eventsource-parser@3.0.5: + resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==} + engines: {node: '>=20.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -15845,6 +15858,12 @@ snapshots: optionalDependencies: graphql: 16.8.1 + '@ai-sdk/gateway@1.0.15(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/gateway@1.0.2(zod@3.25.75)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -15857,20 +15876,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.1(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/openai@2.0.2(zod@3.25.76)': + '@ai-sdk/openai@2.0.22(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.0(zod@3.25.76) + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.0(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 - eventsource-parser: 3.0.3 - zod: 3.25.76 - zod-to-json-schema: 3.24.5(zod@3.25.76) - '@ai-sdk/provider-utils@3.0.1(zod@3.25.75)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -15887,6 +15898,13 @@ snapshots: zod: 3.25.76 zod-to-json-schema: 3.24.5(zod@3.25.76) + '@ai-sdk/provider-utils@3.0.7(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.5 + zod: 3.25.76 + '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 @@ -18973,11 +18991,11 @@ snapshots: - sql.js - sqlite3 - '@folo-services/ai-tools@0.2.20(@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.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)': dependencies: - '@ai-sdk/openai': 2.0.2(zod@3.25.76) - '@folo-services/drizzle': 0.1.13(@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.4(zod@3.25.76) + '@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) + 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 transitivePeerDependencies: @@ -19016,45 +19034,6 @@ snapshots: dependencies: zod: 3.25.76 - '@folo-services/drizzle@0.1.13(@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.11 - 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) - drizzle-zod: 0.7.1(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) - nanoid: 5.1.5 - pg: 8.16.3 - zod: 3.25.76 - transitivePeerDependencies: - - '@aws-sdk/client-rds-data' - - '@cloudflare/workers-types' - - '@electric-sql/pglite' - - '@libsql/client' - - '@libsql/client-wasm' - - '@neondatabase/serverless' - - '@op-engineering/op-sqlite' - - '@opentelemetry/api' - - '@planetscale/database' - - '@prisma/client' - - '@tidbcloud/serverless' - - '@types/better-sqlite3' - - '@types/pg' - - '@types/sql.js' - - '@upstash/redis' - - '@vercel/postgres' - - '@xata.io/client' - - better-sqlite3 - - bun-types - - expo-sqlite - - gel - - knex - - kysely - - mysql2 - - pg-native - - postgres - - prisma - - sql.js - - sqlite3 - '@folo-services/drizzle@0.1.21(@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 @@ -19094,7 +19073,44 @@ snapshots: - sql.js - sqlite3 - '@folo-services/exceptions@0.1.11': {} + '@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)': + 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) + drizzle-zod: 0.7.1(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) + nanoid: 5.1.5 + pg: 8.16.3 + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 '@folo-services/exceptions@0.1.16': {} @@ -22653,6 +22669,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai@5.0.26(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 1.0.15(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ai@5.0.4(zod@3.25.75): dependencies: '@ai-sdk/gateway': 1.0.2(zod@3.25.75) @@ -25633,6 +25657,8 @@ snapshots: eventsource-parser@3.0.3: {} + eventsource-parser@3.0.5: {} + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5