feat(ai): update client SDK and enhance AI settings management

- Upgraded '@follow-app/client-sdk' to version 0.3.40, reflecting the latest changes in AI model management.
- Introduced new sections for managing AI shortcuts, token usage, and MCP services within the settings interface.
- Added components for personalizing prompts and managing panel styles, improving user interaction with AI features.
- Enhanced the overall layout and organization of AI settings for better usability.

These changes collectively improve the user experience by providing a more intuitive interface for managing AI settings and enhancing the overall functionality of the AI chat feature.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-08-20 23:19:01 +08:00
parent a3a6a2aedb
commit f086dc43f6
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
16 changed files with 1072 additions and 1005 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
import { useTranslation } from "react-i18next"
import { AIChatPanelStyle, setAIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai"
import { SettingTabbedSegment } from "../../control"
export const PanelStyleSection = () => {
const { t } = useTranslation("ai")
const panelStyle = useAIChatPanelStyle()
return (
<SettingTabbedSegment
key="panel-style"
label={t("settings.panel_style.label")}
description={t("settings.panel_style.description")}
value={panelStyle}
values={[
{
value: AIChatPanelStyle.Fixed,
label: t("settings.panel_style.fixed"),
icon: <i className="i-mingcute-rectangle-vertical-line" />,
},
{
value: AIChatPanelStyle.Floating,
label: t("settings.panel_style.floating"),
icon: <i className="i-mingcute-layout-right-line" />,
},
]}
onValueChanged={(value) => {
setAIChatPanelStyle(value as AIChatPanelStyle)
}}
/>
)
}

View File

@ -0,0 +1,111 @@
import { Spring } from "@follow/components/constants/spring.js"
import { Button } from "@follow/components/ui/button/index.js"
import { TextArea } from "@follow/components/ui/input/index.js"
import { Label } from "@follow/components/ui/label/index.jsx"
import { AnimatePresence } from "motion/react"
import * as React from "react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { setAISetting, useAISettingValue } from "~/atoms/settings/ai"
import { m } from "~/components/common/Motion"
import { SettingDescription } from "../../control"
import { SettingModalContentPortal } from "../../modal/layout"
export const PersonalizePromptSection = () => {
const { t } = useTranslation("ai")
const aiSettings = useAISettingValue()
const [prompt, setPrompt] = useState(aiSettings.personalizePrompt)
const [isSaving, setIsSaving] = useState(false)
const MAX_CHARACTERS = 500
const currentLength = prompt.length
const isOverLimit = currentLength > MAX_CHARACTERS
const hasChanges = prompt !== aiSettings.personalizePrompt
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target
// Allow typing but show validation error if over limit
setPrompt(value)
}
const handleSave = async () => {
if (isOverLimit) {
toast.error(`Prompt must be ${MAX_CHARACTERS} characters or less`)
return
}
setIsSaving(true)
try {
setAISetting("personalizePrompt", prompt)
toast.success(t("personalize.saved"))
} finally {
setIsSaving(false)
}
}
return (
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-text text-sm font-medium">{t("personalize.prompt.label")}</Label>
<div className="relative -mx-3">
<TextArea
value={prompt}
onChange={handlePromptChange}
placeholder={t("personalize.prompt.placeholder")}
className={`min-h-[80px] resize-none text-sm ${
isOverLimit ? "border-red focus:border-red" : ""
}`}
/>
<div
className={`absolute bottom-2 right-2 text-xs ${
isOverLimit
? "text-red"
: currentLength > MAX_CHARACTERS * 0.8
? "text-yellow"
: "text-text-tertiary"
}`}
>
{currentLength}/{MAX_CHARACTERS}
</div>
</div>
<SettingDescription>
{t("personalize.prompt.help")}
{isOverLimit && (
<span className="text-red mt-1 block">
Prompt exceeds {MAX_CHARACTERS} character limit
</span>
)}
</SettingDescription>
</div>
<AnimatePresence>
{hasChanges && (
<SettingModalContentPortal>
<m.div
initial={{ y: 20, scale: 0.95 }}
animate={{ y: 0, scale: 1 }}
exit={{ y: 20, scale: 0.95 }}
transition={Spring.presets.snappy}
className="absolute inset-x-0 bottom-3 z-10 flex justify-center px-3"
>
<div className="backdrop-blur-background bg-material-medium border-border shadow-perfect flex w-fit max-w-[92%] items-center justify-between gap-3 rounded-full border py-2 pl-5 pr-2">
<span className="text-text-secondary text-xs sm:text-sm">Unsaved changes</span>
<Button
buttonClassName="bg-accent rounded-full"
size="sm"
onClick={handleSave}
disabled={isSaving || isOverLimit}
>
{isSaving ? "Saving..." : "Save"}
</Button>
</div>
</m.div>
</SettingModalContentPortal>
)}
</AnimatePresence>
</div>
)
}

View File

@ -0,0 +1,84 @@
import { Progress } from "@follow/components/ui/progress/index.jsx"
import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { apiFetch } from "~/lib/api-fetch"
import { SettingDescription } from "../../control"
const useTokenUsage = () => {
return useQuery({
queryKey: ["aiTokenUsage"],
queryFn: async () => {
// TODO: replace with api client call
return (
(await apiFetch("/ai/usage")) as {
code: 0
data: {
total: number
used: number
remaining: number
resetAt: string
}
}
).data
},
})
}
export const TokenUsageSection = () => {
const { t } = useTranslation("ai")
const tokenUsage = useTokenUsage().data || {
total: 0,
used: 0,
remaining: 0,
resetAt: new Date(),
}
const usagePercentage = tokenUsage.total === 0 ? 0 : (tokenUsage.used / tokenUsage.total) * 100
const resetDate = new Date(tokenUsage.resetAt)
return (
<div className="space-y-4">
<SettingDescription>{t("token_usage.description")}</SettingDescription>
<div className="bg-material-medium border-border space-y-4 rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-text text-sm font-medium">
{t("token_usage.tokens_used", {
used: tokenUsage.used.toLocaleString(),
total: tokenUsage.total.toLocaleString(),
})}
</p>
<p className="text-text-secondary text-xs">
{tokenUsage.remaining.toLocaleString()} {t("token_usage.tokens_remaining")}
</p>
</div>
<div className="text-right">
<div className="text-text text-sm font-medium">{Math.round(usagePercentage)}%</div>
</div>
</div>
<div className="space-y-2">
<Progress value={usagePercentage} className="h-2" />
<div className="text-text-tertiary flex items-center justify-between text-xs">
<span>0</span>
<span>
{t("token_usage.resets_at")}: {resetDate.toLocaleDateString()}
</span>
<span>{tokenUsage.total.toLocaleString()}</span>
</div>
</div>
<div className="flex items-center gap-2 pt-2">
<div className="bg-accent size-3 rounded-full" />
<span className="text-text-secondary text-xs">Current usage</span>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,5 @@
export { MCPServicesSection } from "./mcp"
export { PanelStyleSection } from "./PanelStyleSection"
export { PersonalizePromptSection } from "./PersonalizePromptSection"
export { AIShortcutsSection } from "./shortcuts"
export { TokenUsageSection } from "./TokenUsageSection"

View File

@ -0,0 +1,109 @@
import { Button } from "@follow/components/ui/button/index.js"
import type { MCPService } from "@follow/shared/settings/interface"
import { useTranslation } from "react-i18next"
interface MCPServiceItemProps {
service: MCPService
onDelete: (id: string) => void
onRefresh: (connectionId: string) => void
onEdit: (service: MCPService) => void
isDeleting?: boolean
isRefreshing?: boolean
}
export const MCPServiceItem = ({
service,
onDelete,
onRefresh,
onEdit,
isDeleting = false,
isRefreshing = false,
}: MCPServiceItemProps) => {
const { t } = useTranslation("ai")
const getConnectionStatusColor = (isConnected: boolean) => {
return isConnected ? "bg-green/10 text-green" : "bg-gray/10 text-text-tertiary"
}
const getConnectionStatusText = (isConnected: boolean) => {
return isConnected
? t("integration.mcp.service.connected")
: t("integration.mcp.service.disconnected")
}
const formatDate = (dateString: string | null) => {
if (!dateString) return "Never"
return new Date(dateString).toLocaleDateString()
}
return (
<div className="hover:bg-material-medium border-border group rounded-lg border p-4 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h4 className="text-text text-sm font-medium">{service.name}</h4>
<div
className={`rounded-full px-2 py-1 text-xs ${getConnectionStatusColor(service.isConnected)}`}
>
{getConnectionStatusText(service.isConnected)}
</div>
<div className="bg-blue/10 text-blue rounded-full px-2 py-1 text-xs">
{service.transportType}
</div>
</div>
<div className="space-y-1">
{service.url && (
<p className="text-text-secondary text-xs">
<span className="text-text-tertiary">URL:</span> {service.url}
</p>
)}
<p className="text-text-secondary text-xs">
<span className="text-text-tertiary">Tools:</span> {service.toolCount}
<span className="text-text-tertiary ml-4">Created:</span>{" "}
{formatDate(service.createdAt)}
<span className="text-text-tertiary ml-4">Last Used:</span>{" "}
{formatDate(service.lastUsed)}
</p>
{service.lastError && (
<p className="text-red text-xs">
<span className="text-text-tertiary">Error:</span> {service.lastError}
</p>
)}
</div>
</div>
<div className="ml-4 flex items-center gap-1 opacity-60 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="sm" onClick={() => onEdit(service)} title="Edit connection">
<i className="i-mgc-edit-cute-re size-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onRefresh(service.id)}
title="Refresh tools"
disabled={isRefreshing}
>
{isRefreshing ? (
<i className="i-mgc-loading-3-cute-re size-4 animate-spin" />
) : (
<i className="i-mgc-refresh-2-cute-re size-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(service.id)}
disabled={isDeleting}
>
{isDeleting ? (
<i className="i-mgc-loading-3-cute-re size-4 animate-spin" />
) : (
<i className="i-mgc-delete-2-cute-re size-4" />
)}
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,142 @@
import { Button } from "@follow/components/ui/button/index.js"
import { Input } from "@follow/components/ui/input/index.js"
import { KeyValueEditor } from "@follow/components/ui/key-value-editor/index.js"
import { Label } from "@follow/components/ui/label/index.jsx"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@follow/components/ui/select/index.js"
import type { MCPService } from "@follow/shared/settings/interface"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
interface MCPServiceModalContentProps {
service?: MCPService | null
onSave: (service: {
name: string
transportType: "streamable-http" | "sse"
url: string
headers?: Record<string, string>
}) => void
onCancel: () => void
isLoading?: boolean
}
export const MCPServiceModalContent = ({
service,
onSave,
onCancel,
isLoading = false,
}: MCPServiceModalContentProps) => {
const { t } = useTranslation("ai")
const [name, setName] = useState(service?.name || "")
const [url, setUrl] = useState(service?.url || "")
const [transportType, setTransportType] = useState<"streamable-http" | "sse">(
service?.transportType || "streamable-http",
)
const [headers, setHeaders] = useState<Record<string, string>>(service?.headers || {})
const handleSave = () => {
if (!name.trim()) {
toast.error(t("integration.mcp.service.validation.name_required"))
return
}
if (!url.trim()) {
toast.error(t("integration.mcp.service.validation.baseUrl_required"))
return
}
// Basic URL validation
try {
new URL(url.trim())
} catch {
toast.error(t("integration.mcp.service.validation.invalid_url"))
return
}
onSave({
name: name.trim(),
transportType,
url: url.trim(),
headers: Object.keys(headers).length > 0 ? headers : undefined,
})
}
return (
<div className="space-y-4">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label className="text-text text-xs">{t("integration.mcp.service.name")}</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("integration.mcp.service.name_placeholder")}
/>
</div>
<div className="space-y-2">
<Label className="text-text text-xs">Transport Type</Label>
<Select
value={transportType}
onValueChange={(value) => setTransportType(value as "streamable-http" | "sse")}
>
<SelectTrigger>
<SelectValue placeholder="Select transport type" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="streamable-http">Streamable HTTP</SelectItem>
<SelectItem value="sse">Server-Sent Events</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-text text-xs">URL</Label>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/mcp"
/>
</div>
<div className="min-w-[500px] space-y-2">
<Label className="text-text text-xs">Headers (Optional)</Label>
<KeyValueEditor
value={headers}
onChange={setHeaders}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addButtonText="Add Header"
minRows={0}
/>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<div />
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
<Button size="sm" onClick={handleSave} disabled={isLoading}>
{isLoading ? (
<>
<i className="i-mgc-loading-3-cute-re mr-2 size-4 animate-spin" />
Saving...
</>
) : (
"Save"
)}
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,282 @@
import { Button } from "@follow/components/ui/button/index.js"
import { Label } from "@follow/components/ui/label/index.jsx"
import { Switch } from "@follow/components/ui/switch/index.jsx"
import type { MCPService } from "@follow/shared/settings/interface"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import * as React from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { setMCPEnabled, useMCPEnabled } from "~/atoms/settings/ai"
import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
import {
createMCPConnection,
deleteMCPConnection,
fetchMCPConnections,
mcpQueryKeys,
refreshMCPTools,
updateMCPConnection,
} from "~/queries/mcp"
import { MCPServiceItem } from "./MCPServiceItem"
import { MCPServiceModalContent } from "./MCPServiceModalContent"
export const MCPServicesSection = () => {
const { t } = useTranslation("ai")
const mcpEnabled = useMCPEnabled()
const queryClient = useQueryClient()
const dialog = useDialog()
// Reusable OAuth authorization handler using dialog
const handleOAuthAuthorization = async (authorizationUrl: string, connectionId?: string) => {
const confirmed = await dialog.ask({
title: t("integration.mcp.service.auth_required"),
message: t("integration.mcp.service.auth_message"),
confirmText: t("integration.mcp.service.open_auth"),
cancelText: t("words.cancel", { ns: "common" }),
variant: "ask",
})
if (confirmed) {
const popup = window.open(
authorizationUrl,
"_blank",
"width=600,height=700,scrollbars=yes,resizable=yes",
)
if (!popup) {
toast.error(t("integration.mcp.service.popup_blocked"))
} else {
popup.onclose = () => {
// FIXME
setTimeout(() => {
refreshToolsMutation.mutate(connectionId ? [connectionId] : undefined)
}, 3000)
}
}
}
}
// Query for MCP connections
const {
data: mcpServices = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: mcpQueryKeys.connections(),
queryFn: fetchMCPConnections,
enabled: mcpEnabled,
refetchInterval: 30_000,
refetchOnWindowFocus: true,
retry: 2,
})
// Mutation for creating MCP connection
const createConnectionMutation = useMutation({
mutationFn: createMCPConnection,
onSuccess: async (result) => {
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
// Handle OAuth authorization if needed
if (result.authorizationUrl) {
await handleOAuthAuthorization(result.authorizationUrl, result.connectionId)
} else {
toast.success(t("integration.mcp.service.added"))
refreshToolsMutation.mutate([result.connectionId])
}
},
onError: (error) => {
toast.error(t("integration.mcp.service.discovery_failed"))
console.error("Failed to create MCP connection:", error)
},
})
// Mutation for updating MCP connection
const updateConnectionMutation = useMutation({
mutationFn: ({
connectionId,
updateData,
}: {
connectionId: string
updateData: Parameters<typeof updateMCPConnection>[1]
}) => updateMCPConnection(connectionId, updateData),
onSuccess: async (result) => {
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
// Handle OAuth authorization if needed
if (result.authorizationUrl) {
await handleOAuthAuthorization(result.authorizationUrl, result.connectionId)
} else {
toast.success(t("integration.mcp.service.updated"))
refreshToolsMutation.mutate([result.connectionId])
}
},
onError: (error) => {
toast.error("Failed to update MCP connection")
console.error("Failed to update MCP connection:", error)
},
})
// Mutation for deleting MCP connection
const deleteConnectionMutation = useMutation({
mutationFn: deleteMCPConnection,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
toast.success(t("integration.mcp.service.deleted"))
},
onError: (error) => {
toast.error("Failed to delete MCP connection")
console.error("Failed to delete MCP connection:", error)
},
})
// Mutation for refreshing MCP tools
const refreshToolsMutation = useMutation({
mutationFn: (connectionIds?: string[]) => refreshMCPTools(connectionIds),
onSuccess: () => {
// Invalidate both connections (for updated counts) and tools queries
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.all })
toast.success("MCP tools refreshed successfully")
},
onError: (error) => {
toast.error("Failed to refresh MCP tools")
console.error("Failed to refresh MCP tools:", error)
},
})
const { present } = useModalStack()
const handleAddService = () => {
present({
title: "Add MCP Service",
content: ({ dismiss }: { dismiss: () => void }) => (
<MCPServiceModalContent
service={null}
onSave={(service) => {
createConnectionMutation.mutate(service)
dismiss()
}}
onCancel={dismiss}
isLoading={createConnectionMutation.isPending}
/>
),
})
}
const handleEditService = (service: MCPService) => {
present({
title: "Edit MCP Service",
content: ({ dismiss }: { dismiss: () => void }) => (
<MCPServiceModalContent
service={service}
onSave={(updatedService) => {
updateConnectionMutation.mutate({
connectionId: service.id,
updateData: updatedService,
})
dismiss()
}}
onCancel={dismiss}
isLoading={updateConnectionMutation.isPending}
/>
),
})
}
const handleDeleteService = (id: string) => {
deleteConnectionMutation.mutate(id)
}
const handleRefreshTools = (connectionId?: string) => {
refreshToolsMutation.mutate(connectionId ? [connectionId] : undefined)
}
// Show error message if query failed
React.useEffect(() => {
if (error) {
toast.error("Failed to load MCP connections")
console.error("Failed to load MCP connections:", error)
}
}, [error])
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-text text-sm font-medium">{t("integration.mcp.enabled")}</Label>
<div className="text-text-secondary text-xs">{t("integration.mcp.description")}</div>
</div>
<Switch checked={mcpEnabled} onCheckedChange={setMCPEnabled} />
</div>
</div>
{mcpEnabled && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-text text-sm font-medium">
{t("integration.mcp.services.title")}
</Label>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => refetch()}
disabled={isLoading}
title="Refresh connections"
>
{isLoading ? (
<i className="i-mgc-loading-3-cute-re size-4 animate-spin" />
) : (
<i className="i-mgc-refresh-2-cute-re size-4" />
)}
</Button>
<Button variant="outline" size="sm" onClick={handleAddService}>
<i className="i-mgc-add-cute-re mr-2 size-4" />
{t("integration.mcp.services.add")}
</Button>
</div>
</div>
{mcpServices.length === 0 && (
<div className="py-8 text-center">
<div className="bg-fill-secondary mx-auto mb-3 flex size-12 items-center justify-center rounded-full">
<i className="i-mgc-plugin-2-cute-re text-text size-6" />
</div>
<h4 className="text-text mb-1 text-sm font-medium">
{t("integration.mcp.services.empty.title")}
</h4>
<p className="text-text-secondary text-xs">
{t("integration.mcp.services.empty.description")}
</p>
</div>
)}
{isLoading && (
<div className="flex items-center justify-center py-8">
<i className="i-mgc-loading-3-cute-re size-6 animate-spin" />
</div>
)}
{mcpServices.map((service) => (
<MCPServiceItem
key={service.id}
service={service}
onDelete={handleDeleteService}
onRefresh={handleRefreshTools}
onEdit={handleEditService}
isDeleting={
deleteConnectionMutation.isPending &&
deleteConnectionMutation.variables === service.id
}
isRefreshing={
refreshToolsMutation.isPending && refreshToolsMutation.variables?.[0] === service.id
}
/>
))}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,3 @@
export { MCPServiceItem } from "./MCPServiceItem"
export { MCPServiceModalContent } from "./MCPServiceModalContent"
export { MCPServicesSection } from "./MCPServicesSection"

View File

@ -0,0 +1,104 @@
import type { AIShortcut } from "@follow/shared/settings/interface"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { setAISetting, useAISettingValue } from "~/atoms/settings/ai"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { SettingActionItem } from "../../../control"
import { ShortcutItem } from "./ShortcutItem"
import { ShortcutModalContent } from "./ShortcutModalContent"
export const AIShortcutsSection = () => {
const { t } = useTranslation("ai")
const { shortcuts } = useAISettingValue()
const { present } = useModalStack()
const handleAddShortcut = () => {
present({
title: "Add AI Shortcut",
content: ({ dismiss }: { dismiss: () => void }) => (
<ShortcutModalContent
shortcut={null}
onSave={(shortcut) => {
const newShortcut: AIShortcut = {
...shortcut,
id: Date.now().toString(),
}
setAISetting("shortcuts", [...shortcuts, newShortcut])
toast.success(t("shortcuts.added"))
dismiss()
}}
onCancel={dismiss}
/>
),
})
}
const handleEditShortcut = (shortcut: AIShortcut) => {
present({
title: "Edit AI Shortcut",
content: ({ dismiss }: { dismiss: () => void }) => (
<ShortcutModalContent
shortcut={shortcut}
onSave={(updatedShortcut) => {
setAISetting(
"shortcuts",
shortcuts.map((s) =>
s.id === shortcut.id ? { ...updatedShortcut, id: shortcut.id } : s,
),
)
toast.success(t("shortcuts.updated"))
dismiss()
}}
onCancel={dismiss}
/>
),
})
}
const handleDeleteShortcut = (id: string) => {
setAISetting(
"shortcuts",
shortcuts.filter((s) => s.id !== id),
)
toast.success(t("shortcuts.deleted"))
}
const handleToggleShortcut = (id: string, enabled: boolean) => {
setAISetting(
"shortcuts",
shortcuts.map((s) => (s.id === id ? { ...s, enabled } : s)),
)
}
return (
<div className="space-y-4">
<SettingActionItem
label={t("shortcuts.add")}
action={handleAddShortcut}
buttonText={t("shortcuts.add")}
/>
{shortcuts.length === 0 && (
<div className="py-8 text-center">
<div className="bg-fill-secondary mx-auto mb-3 flex size-12 items-center justify-center rounded-full">
<i className="i-mgc-magic-2-cute-re text-text size-6" />
</div>
<h4 className="text-text mb-1 text-sm font-medium">{t("shortcuts.empty.title")}</h4>
<p className="text-text-secondary text-xs">{t("shortcuts.empty.description")}</p>
</div>
)}
{shortcuts.map((shortcut) => (
<ShortcutItem
key={shortcut.id}
shortcut={shortcut}
onDelete={handleDeleteShortcut}
onToggle={handleToggleShortcut}
onEdit={handleEditShortcut}
/>
))}
</div>
)
}

View File

@ -0,0 +1,54 @@
import { Button } from "@follow/components/ui/button/index.js"
import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
import { Switch } from "@follow/components/ui/switch/index.jsx"
import type { AIShortcut } from "@follow/shared/settings/interface"
interface ShortcutItemProps {
shortcut: AIShortcut
onDelete: (id: string) => void
onToggle: (id: string, enabled: boolean) => void
onEdit: (shortcut: AIShortcut) => void
}
export const ShortcutItem = ({ shortcut, onDelete, onToggle, onEdit }: ShortcutItemProps) => {
return (
<div className="hover:bg-material-medium border-border group rounded-lg border p-4 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h4 className="text-text text-sm font-medium">{shortcut.name}</h4>
{shortcut.hotkey && (
<KbdCombined kbdProps={{ wrapButton: false }} joint={false}>
{shortcut.hotkey}
</KbdCombined>
)}
</div>
<p className="text-text-secondary line-clamp-2 text-xs leading-relaxed">
{shortcut.prompt}
</p>
</div>
<div className="ml-4 flex items-center gap-3">
<div className="flex items-center gap-1 opacity-60 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="sm" onClick={() => onEdit(shortcut)}>
<i className="i-mgc-edit-cute-re size-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDelete(shortcut.id)}>
<i className="i-mgc-delete-2-cute-re size-4" />
</Button>
</div>
<div className="border-fill-tertiary flex items-center gap-2 border-l pl-3">
<span className="text-text-tertiary text-xs font-medium">
{shortcut.enabled ? "ON" : "OFF"}
</span>
<Switch
checked={shortcut.enabled}
onCheckedChange={(enabled) => onToggle(shortcut.id, enabled)}
/>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,105 @@
import { Button } from "@follow/components/ui/button/index.js"
import { Input, TextArea } from "@follow/components/ui/input/index.js"
import { Label } from "@follow/components/ui/label/index.jsx"
import { Switch } from "@follow/components/ui/switch/index.jsx"
import type { AIShortcut } from "@follow/shared/settings/interface"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
interface ShortcutModalContentProps {
shortcut?: AIShortcut | null
onSave: (shortcut: Omit<AIShortcut, "id">) => void
onCancel: () => void
}
export const ShortcutModalContent = ({ shortcut, onSave, onCancel }: ShortcutModalContentProps) => {
const { t } = useTranslation("ai")
const [name, setName] = useState(shortcut?.name || "")
const [prompt, setPrompt] = useState(shortcut?.prompt || "")
const [enabled, setEnabled] = useState(shortcut?.enabled ?? true)
const handleSave = () => {
if (!name.trim() || !prompt.trim()) {
toast.error(t("shortcuts.validation.required"))
return
}
onSave({
name: name.trim(),
prompt: prompt.trim(),
enabled,
})
}
return (
<div className="w-[400px] space-y-4">
<div className="grid grid-cols-6 gap-4">
<div className="col-span-6 space-y-2">
<Label className="text-text text-xs">{t("shortcuts.name")}</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("shortcuts.name_placeholder")}
/>
</div>
{/* <div className="col-span-2 space-y-2">
<Label className="text-text text-xs">{t("shortcuts.hotkey")}</Label>
<button
type="button"
className="border-border hover:bg-material-medium flex h-9 w-full items-center rounded-md border bg-transparent px-3 py-2 text-sm transition-colors focus:outline-none"
onClick={() => setIsRecording(!isRecording)}
>
{isRecording ? (
<KeyRecorder
onBlur={() => setIsRecording(false)}
onChange={(keys) => {
setHotkey(Array.isArray(keys) ? keys.join("+") : "")
setIsRecording(false)
}}
/>
) : (
<div className="flex w-full items-center justify-center">
<div className="flex items-center justify-center gap-2">
{hotkey ? (
<KbdCombined kbdProps={{ wrapButton: false }} joint={false}>
{hotkey}
</KbdCombined>
) : (
<span className="text-text-tertiary text-xs">Click to record</span>
)}
</div>
</div>
)}
</button>
</div> */}
</div>
<div className="space-y-2">
<Label className="text-text text-xs">{t("shortcuts.prompt")}</Label>
<TextArea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder={t("shortcuts.prompt_placeholder")}
className="min-h-[60px] resize-none text-sm"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch checked={enabled} onCheckedChange={setEnabled} />
<Label className="text-text text-xs">{t("shortcuts.enabled")}</Label>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onCancel}>
Cancel
</Button>
<Button size="sm" onClick={handleSave}>
Save
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,3 @@
export { AIShortcutsSection } from "./AIShortcutsSection"
export { ShortcutItem } from "./ShortcutItem"
export { ShortcutModalContent } from "./ShortcutModalContent"

View File

@ -2,15 +2,13 @@ import type { MCPService } from "@follow/shared/settings/interface"
import { followApi } from "~/lib/api-client"
// MCP Service API calls - these connect to the actual server endpoints via followClient
export const createMCPConnection = async (connectionData: {
name: string
transportType: "streamable-http" | "sse"
url: string
headers?: Record<string, string>
}): Promise<{ authorizationUrl?: string }> => {
const res = await followApi.mcp.createConnection(connectionData)
return { authorizationUrl: res.authorizationUrl }
}) => {
return followApi.mcp.createConnection(connectionData)
}
export const fetchMCPConnections = async (): Promise<MCPService[]> => {
@ -26,9 +24,8 @@ export const updateMCPConnection = async (
url?: string
headers?: Record<string, string>
},
): Promise<{ authorizationUrl?: string }> => {
const res = await followApi.mcp.updateConnection({ connectionId, ...updateData })
return { authorizationUrl: res.authorizationUrl }
) => {
return followApi.mcp.updateConnection({ connectionId, ...updateData })
}
export const deleteMCPConnection = async (connectionId: string): Promise<void> => {

View File

@ -7,8 +7,8 @@ settings:
catalogs:
default:
'@follow-app/client-sdk':
specifier: 0.3.39
version: 0.3.39
specifier: 0.3.40
version: 0.3.40
tailwindcss-uikit-colors:
specifier: 1.0.0
version: 1.0.0
@ -465,7 +465,7 @@ importers:
version: 3.0.2(electron@37.2.0)
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.39(@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)
version: 0.3.40(@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)
'@follow/database':
specifier: workspace:*
version: link:../../../../packages/internal/database
@ -1673,7 +1673,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.39(@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)
version: 0.3.40(@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)
'@follow/configs':
specifier: workspace:*
version: link:../../configs
@ -1685,7 +1685,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.39(@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)
version: 0.3.40(@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)
'@follow/constants':
specifier: workspace:*
version: link:../constants
@ -1763,7 +1763,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.39(@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)
version: 0.3.40(@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)
'@follow/constants':
specifier: workspace:*
version: link:../constants
@ -1827,7 +1827,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.39(@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)
version: 0.3.40(@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)
'@follow/configs':
specifier: workspace:*
version: link:../../configs
@ -4051,20 +4051,20 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@follow-app/client-sdk@0.3.39':
resolution: {integrity: sha512-NnjLOzrQ2UD6LgwH6ItK0SJts5UinzLCXJFJf74X096a6qd9xhRuHJ3X+Ea3qNw/6WXcK4l3dnkHBHDaDfthRA==}
'@follow-app/client-sdk@0.3.40':
resolution: {integrity: sha512-AsSZlh10NvmskdVU1XuUh3S2rVBmz0qoiMRUtdbdU5/JARtwNM1UiIo1WfaGpinht/ZVsKLsR40p03XovRynmQ==}
'@folo-services/ai-tools@0.2.20':
resolution: {integrity: sha512-1AJI48H2g64JhuTruSqQKUrn4Tc353hMFdKAbohh65AxTAMN/1pIw9w17Dtjm+g5BVlZUXYKaiIvaN9tHsXLNA==}
'@folo-services/constants@0.1.22':
resolution: {integrity: sha512-zF3yX7lxTpAsZQjp8+P56nEIa7IqDVIyqMZcNpFLm8UHTS2ltPeM/S9KdNbacz9lxDmWjjCmZdEfuBbs0B5b8Q==}
'@folo-services/constants@0.1.23':
resolution: {integrity: sha512-2tK244KD/RhR6KoLarEPHkMoco3/JvAbL05TCr5pM3pEem7hqCzHNLLDFwAdEl2m1ByTTQHnqqhx/rEZx1BSgA==}
'@folo-services/drizzle@0.1.13':
resolution: {integrity: sha512-h6tndGnTLrAmfvxK5XsXGdZaMi2GBmFZBFiBqtabC7Y05A1ZSOaWg/d9f60Ydh586LmVonz5VGrhvzTdn5ukhg==}
'@folo-services/drizzle@0.1.17':
resolution: {integrity: sha512-g0BgfW+hn6vmQyz9SSTOV5KJVexgFoyEgEgQCeSVM6KxufH88VxVpJzkGA1zb7CxEuWrxTuHmpBEolcNFfABTQ==}
'@folo-services/drizzle@0.1.18':
resolution: {integrity: sha512-mCpxdUGpkLJ6YoohpYarkQR0fobCY1ZUZmDqyHxk9p6PtzyD/WziXYUAs9IcCuICCUiIt3dyiPie+lx/Nx3hMw==}
'@folo-services/exceptions@0.1.11':
resolution: {integrity: sha512-OMKat8KcIxjw9pBfG3pC1kRcIs0YiuaYmMDA2MRy8AEGcki8F9u5e5QH7OKgP/LlNR7Fs2Csz1XetvcC4nB+RQ==}
@ -4072,8 +4072,8 @@ packages:
'@folo-services/exceptions@0.1.14':
resolution: {integrity: sha512-VFVRkzXg7BytEvdg7iyUXc6qaAudTTtOU8PrTkRUG4ZB9udLE/uVT5q+6poS1CkwMi2GroH27AFC4fnfVp6Fvw==}
'@folo-services/shared@0.0.12':
resolution: {integrity: sha512-CHBdwFrke6Mbwxu5MWw9tB7q9CICG2Vh/9IZ8BIa1M65sF/IBZi1sE2roHayuDix0TXXJbceaD+Tv84ROFdO3g==}
'@folo-services/shared@0.0.13':
resolution: {integrity: sha512-bge+IHFzCjr/QonZqQnDudlLc+P8OuimWR0bdRXUgnNjZ9R6jzIxSB5xYqYVY6DnHM+YyIOYnHAIT09Y3la4fw==}
'@fontsource/sn-pro@5.2.5':
resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==}
@ -18933,12 +18933,12 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@follow-app/client-sdk@0.3.39(@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)':
'@follow-app/client-sdk@0.3.40(@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:
'@folo-services/constants': 0.1.22
'@folo-services/drizzle': 0.1.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)
'@folo-services/constants': 0.1.23
'@folo-services/drizzle': 0.1.18(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
'@folo-services/exceptions': 0.1.14
'@folo-services/shared': 0.0.12(@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/shared': 0.0.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)(pg@8.16.3)
zod: 3.25.76
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
@ -19011,7 +19011,7 @@ snapshots:
- sql.js
- sqlite3
'@folo-services/constants@0.1.22':
'@folo-services/constants@0.1.23':
dependencies:
zod: 3.25.76
@ -19054,7 +19054,7 @@ snapshots:
- sql.js
- sqlite3
'@folo-services/drizzle@0.1.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)':
'@folo-services/drizzle@0.1.18(@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.14
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)
@ -19097,9 +19097,9 @@ snapshots:
'@folo-services/exceptions@0.1.14': {}
'@folo-services/shared@0.0.12(@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/shared@0.0.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)(pg@8.16.3)':
dependencies:
'@folo-services/drizzle': 0.1.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)
'@folo-services/drizzle': 0.1.18(@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)
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)
zod: 3.25.76

View File

@ -9,7 +9,7 @@ packages:
catalog:
typescript: 5.8.3
"@follow-app/client-sdk": 0.3.39
"@follow-app/client-sdk": 0.3.40
tailwindcss-uikit-colors: 1.0.0
onlyBuiltDependencies: