feat(ai): support ai attachement (#4316)
* init Signed-off-by: Innei <tukon479@gmail.com> * feat(ai-chat): enhance file handling with category-based icon retrieval - Introduced `getFileCategoryFromMimeType` function to categorize file types based on MIME type, improving the clarity of file handling in the AI chat interface. - Updated `FileAttachmentBlock` and `AIChatContextBar` components to utilize the new categorization function for determining file icons, streamlining the code and enhancing maintainability. - Removed redundant MIME type checks, simplifying the logic for icon assignment. These changes improve the user experience by ensuring accurate file representation and reduce code complexity. Signed-off-by: Innei <tukon479@gmail.com> * feat: preview image * Update avatar upload to use followApi and bump deps * feat: Refactor AI chat context blocks and file upload handling - Introduced new components for rendering AI chat context blocks, including AIDataBlockItem and AIDataBlockPart. - Enhanced file attachment handling by updating the structure of AIChatContextBlock to separate file attachments from other block types. - Implemented a new file upload hook (useFileUpload) to manage file uploads with progress tracking and toast notifications. - Added UploadProgress and CircularProgress components for visual feedback during file uploads. - Improved file processing logic to simulate realistic upload progress and handle errors more effectively. - Updated existing components to utilize the new block structure and file upload logic. - Created utility functions for managing block styles, icons, and labels for better code organization. Signed-off-by: Innei <tukon479@gmail.com> * feat: Enhance chat message rendering with Suspense and add ThinkingIndicator component Signed-off-by: Innei <tukon479@gmail.com> * fix: Remove error check for message metadata in AIChatMessage component Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
6a616f6595
commit
9bb974b8fd
|
|
@ -1 +1 @@
|
||||||
Before you start, you need to read and follow the rules in @../claude.md
|
Before you start, you need to read and follow the rules in @../CLAUDE.md
|
||||||
|
|
|
||||||
|
|
@ -60,16 +60,13 @@ Examples:
|
||||||
- Preferred: `i-mgc-copy-cute-re`, `i-mgc-external-link-cute-re`
|
- Preferred: `i-mgc-copy-cute-re`, `i-mgc-external-link-cute-re`
|
||||||
- Fallback only: `i-mingcute-copy-line` (only if no mgc equivalent exists)
|
- Fallback only: `i-mingcute-copy-line` (only if no mgc equivalent exists)
|
||||||
|
|
||||||
## Animation with Framer Motion
|
## Using Framer Motion
|
||||||
|
|
||||||
- **LazyMotion Integration**: Project uses Framer Motion with LazyMotion for optimized bundle size
|
- **LazyMotion Integration**: Project uses Framer Motion with LazyMotion for optimized bundle size
|
||||||
- **Usage Rule**: Always use `m.` instead of `motion.` when creating animated components
|
- **Usage Rule**: Always use `m.` instead of `motion.` when creating animated components
|
||||||
- **Import**: `import { m } from 'motion/react'`
|
- **Import**: `import { m } from 'motion/react'`
|
||||||
- **Examples**: `m.div`, `m.button`, `m.span` (not `motion.div`, `motion.button`, etc.)
|
- **Examples**: `m.div`, `m.button`, `m.span` (not `motion.div`, `motion.button`, etc.)
|
||||||
- **Benefits**: Reduces bundle size while maintaining all Framer Motion functionality
|
- **Benefits**: Reduces bundle size while maintaining all Framer Motion functionality
|
||||||
|
|
||||||
**Animation Presets**:
|
|
||||||
|
|
||||||
- **Prefer Spring Presets**: Use predefined spring animations from `@follow/components/constants/spring.js`
|
- **Prefer Spring Presets**: Use predefined spring animations from `@follow/components/constants/spring.js`
|
||||||
- **Available Presets Constants**: `Spring.presets.smooth`, `Spring.presets.snappy`, `Spring.presets.bouncy` (extracted from Apple's spring parameters)
|
- **Available Presets Constants**: `Spring.presets.smooth`, `Spring.presets.snappy`, `Spring.presets.bouncy` (extracted from Apple's spring parameters)
|
||||||
- **Usage Example**: `transition={Spring.presets.smooth}` or `transition={Spring.snappy(0.3, 0.1)}`
|
- **Usage Example**: `transition={Spring.presets.smooth}` or `transition={Spring.snappy(0.3, 0.1)}`
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@
|
||||||
"@follow/models": "workspace:*",
|
"@follow/models": "workspace:*",
|
||||||
"@follow/types": "workspace:*",
|
"@follow/types": "workspace:*",
|
||||||
"@follow/utils": "workspace:*",
|
"@follow/utils": "workspace:*",
|
||||||
"@folo-services/ai-tools": "0.2.19",
|
"@folo-services/ai-tools": "0.2.20",
|
||||||
"@types/node": "24.0.10",
|
"@types/node": "24.0.10",
|
||||||
"@vite-pwa/assets-generator": "1.0.0",
|
"@vite-pwa/assets-generator": "1.0.0",
|
||||||
"fake-indexeddb": "6.0.1",
|
"fake-indexeddb": "6.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { apiClient, apiFetch } from "./api-fetch"
|
import { followApi } from "./api-client"
|
||||||
import { getFetchErrorMessage } from "./error-parser"
|
import { getFetchErrorMessage } from "./error-parser"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,19 +10,14 @@ import { getFetchErrorMessage } from "./error-parser"
|
||||||
* @returns Promise<string> - The uploaded image URL
|
* @returns Promise<string> - The uploaded image URL
|
||||||
*/
|
*/
|
||||||
export async function uploadAvatarBlob(blob: Blob): Promise<string> {
|
export async function uploadAvatarBlob(blob: Blob): Promise<string> {
|
||||||
const formData = new FormData()
|
const { url } = await followApi.upload
|
||||||
formData.append("file", blob, "avatar.jpg")
|
.uploadAvatar({
|
||||||
|
file: blob,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(getFetchErrorMessage(err))
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
const res = await apiFetch<{
|
return url
|
||||||
url: string
|
|
||||||
}>(apiClient.upload.avatar.$url().toString(), {
|
|
||||||
method: "POST",
|
|
||||||
|
|
||||||
body: formData,
|
|
||||||
}).catch((err) => {
|
|
||||||
toast.error(getFetchErrorMessage(err))
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.url
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { stopPropagation } from "@follow/utils/dom"
|
||||||
import { useCallback } from "react"
|
import { useCallback } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { useSettingModal } from "../settings/modal/useSettingModal"
|
import { useSettingModal } from "../settings/modal/use-setting-modal-hack"
|
||||||
|
|
||||||
export const NeedActivationToast = (props: { dimiss: () => void }) => {
|
export const NeedActivationToast = (props: { dimiss: () => void }) => {
|
||||||
const settingModalPresent = useSettingModal()
|
const settingModalPresent = useSettingModal()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { Spring } from "@follow/components/constants/spring.js"
|
||||||
|
import { cn } from "@follow/utils"
|
||||||
|
import { AnimatePresence, m } from "motion/react"
|
||||||
|
import type { FC, PropsWithChildren } from "react"
|
||||||
|
import { memo, useCallback, useRef, useState } from "react"
|
||||||
|
|
||||||
|
import { useFileUploadWithDefaults } from "../../hooks/useFileUpload"
|
||||||
|
import { useAIChatStore } from "../../store/AIChatContext"
|
||||||
|
import { UploadProgress } from "../ui/UploadProgress"
|
||||||
|
|
||||||
|
interface GlobalFileDropZoneProps extends PropsWithChildren {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalFileDropZone: FC<GlobalFileDropZoneProps> = memo(({ children, className }) => {
|
||||||
|
const { handleFileDrop } = useFileUploadWithDefaults()
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false)
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
|
const dragCounterRef = useRef(0)
|
||||||
|
|
||||||
|
// Get uploading files from the store
|
||||||
|
const blocks = useAIChatStore()((s) => s.blocks)
|
||||||
|
const uploadingFiles = blocks
|
||||||
|
.filter(
|
||||||
|
(block): block is Extract<typeof block, { type: "fileAttachment" }> =>
|
||||||
|
block.type === "fileAttachment" && block.attachment.uploadStatus === "uploading",
|
||||||
|
)
|
||||||
|
.map((block) => block.attachment)
|
||||||
|
|
||||||
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
dragCounterRef.current += 1
|
||||||
|
|
||||||
|
if (e.dataTransfer.types.includes("Files")) {
|
||||||
|
setIsDragOver(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
dragCounterRef.current -= 1
|
||||||
|
|
||||||
|
if (dragCounterRef.current === 0) {
|
||||||
|
setIsDragOver(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
async (e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
dragCounterRef.current = 0
|
||||||
|
setIsDragOver(false)
|
||||||
|
|
||||||
|
const { files } = e.dataTransfer
|
||||||
|
if (!files || files.length === 0) return
|
||||||
|
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleFileDrop(files)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing files:", error)
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleFileDrop],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("relative size-full", className)}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Global Drag Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isDragOver && (
|
||||||
|
<m.div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* Glass morphism backdrop */}
|
||||||
|
<m.div
|
||||||
|
className="bg-material-thin/80 absolute inset-0 backdrop-blur-xl"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={Spring.presets.smooth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<m.div
|
||||||
|
initial={{ scale: 0.9, y: 20, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, y: 0, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.9, y: 20, opacity: 0 }}
|
||||||
|
transition={Spring.presets.snappy}
|
||||||
|
className="bg-background/95 border-accent/20 shadow-accent/10 relative flex max-w-md flex-col items-center gap-4 rounded-2xl border p-8 shadow-2xl"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<div className="border-accent size-12 animate-spin rounded-full border-4 border-t-transparent" />
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-text text-lg font-medium">Processing files...</p>
|
||||||
|
<p className="text-text-secondary text-sm">
|
||||||
|
Please wait while we process your files
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-accent relative">
|
||||||
|
<i className="i-mgc-file-upload-cute-re size-16" />
|
||||||
|
<m.div
|
||||||
|
className="text-accent absolute inset-0 blur-lg"
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.1, 1],
|
||||||
|
opacity: [0.5, 1, 0.5],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="i-mgc-file-upload-cute-re size-16" />
|
||||||
|
</m.div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-text text-lg font-medium">Drop files to attach</p>
|
||||||
|
<p className="text-text-secondary text-sm">
|
||||||
|
Images, PDFs, text files, and audio files are supported
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</m.div>
|
||||||
|
</m.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Upload Progress Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{uploadingFiles.length > 0 && (
|
||||||
|
<m.div className="pointer-events-none fixed bottom-4 right-4 z-40 max-w-sm">
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
transition={Spring.presets.snappy}
|
||||||
|
className="bg-background/95 border-border shadow-popover rounded-lg border p-4 shadow-lg backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
<i className="i-mgc-file-upload-cute-re text-accent size-5" />
|
||||||
|
<span className="text-text text-sm font-medium">
|
||||||
|
Uploading {uploadingFiles.length} file{uploadingFiles.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{uploadingFiles.map((file) => (
|
||||||
|
<div key={file.id} className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-text-secondary truncate text-xs" title={file.name}>
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-text-tertiary text-xs">
|
||||||
|
{file.uploadProgress ? Math.round(file.uploadProgress) : 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<UploadProgress
|
||||||
|
progress={file.uploadProgress || 0}
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</m.div>
|
||||||
|
</m.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
GlobalFileDropZone.displayName = "GlobalFileDropZone"
|
||||||
|
|
@ -7,7 +7,7 @@ import { stopPropagation } from "@follow/utils"
|
||||||
import { cn } from "@follow/utils/utils"
|
import { cn } from "@follow/utils/utils"
|
||||||
import Fuse from "fuse.js"
|
import Fuse from "fuse.js"
|
||||||
import type { FC } from "react"
|
import type { FC } from "react"
|
||||||
import { memo, useMemo, useState } from "react"
|
import { memo, useCallback, useMemo, useRef, useState } from "react"
|
||||||
import { useDebounceCallback } from "usehooks-ts"
|
import { useDebounceCallback } from "usehooks-ts"
|
||||||
|
|
||||||
import { useAISettingValue } from "~/atoms/settings/ai"
|
import { useAISettingValue } from "~/atoms/settings/ai"
|
||||||
|
|
@ -26,13 +26,19 @@ import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext"
|
||||||
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
|
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
|
||||||
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
|
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
|
||||||
|
|
||||||
|
import { useFileUploadWithDefaults } from "../../hooks/useFileUpload"
|
||||||
import { useChatBlockActions } from "../../store/hooks"
|
import { useChatBlockActions } from "../../store/hooks"
|
||||||
|
import { getFileCategoryFromMimeType, getFileIconName } from "../../utils/file-validation"
|
||||||
|
import { CircularProgress } from "../ui/UploadProgress"
|
||||||
|
import { ImageThumbnail } from "./ImageThumbnail"
|
||||||
|
|
||||||
export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => void }> = memo(
|
export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => void }> = memo(
|
||||||
({ className, onSendShortcut }) => {
|
({ className, onSendShortcut }) => {
|
||||||
const blocks = useAIChatStore()((s) => s.blocks)
|
const blocks = useAIChatStore()((s) => s.blocks)
|
||||||
const blockActions = useChatBlockActions()
|
const blockActions = useChatBlockActions()
|
||||||
const { shortcuts } = useAISettingValue()
|
const { shortcuts } = useAISettingValue()
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { handleFileInputChange } = useFileUploadWithDefaults()
|
||||||
|
|
||||||
// Filter enabled shortcuts
|
// Filter enabled shortcuts
|
||||||
const enabledShortcuts = useMemo(
|
const enabledShortcuts = useMemo(
|
||||||
|
|
@ -41,6 +47,10 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) =>
|
||||||
)
|
)
|
||||||
|
|
||||||
const showSettingModal = useSettingModal()
|
const showSettingModal = useSettingModal()
|
||||||
|
|
||||||
|
const handleAttachFile = useCallback(() => {
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}, [])
|
||||||
const contextMenuContent = (
|
const contextMenuContent = (
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
|
|
@ -135,6 +145,26 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) =>
|
||||||
{contextMenuContent}
|
{contextMenuContent}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* File Upload Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAttachFile}
|
||||||
|
className="bg-fill-secondary hover:bg-fill-tertiary border-border text-text-tertiary hover:text-text-secondary flex size-7 items-center justify-center rounded-md border transition-colors"
|
||||||
|
title="Upload Files"
|
||||||
|
>
|
||||||
|
<i className="i-mgc-attachment-cute-re size-3.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Hidden File Input */}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*,.pdf,.txt,.md"
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* AI Shortcuts Button */}
|
{/* AI Shortcuts Button */}
|
||||||
{enabledShortcuts.length > 0 && (
|
{enabledShortcuts.length > 0 && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
@ -231,7 +261,10 @@ const PickerList = <T extends PickerItem>({
|
||||||
const CurrentFeedEntriesPickerList: FC<{ onSelect: (entryId: string) => void }> = ({
|
const CurrentFeedEntriesPickerList: FC<{ onSelect: (entryId: string) => void }> = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const mainEntryId = useAIChatStore()((s) => s.blocks.find((b) => b.type === "mainEntry")?.value)
|
const mainEntryId = useAIChatStore()((s) => {
|
||||||
|
const block = s.blocks.find((b) => b.type === "mainEntry")
|
||||||
|
return block && block.type === "mainEntry" ? block.value : undefined
|
||||||
|
})
|
||||||
const feedId = useEntry(mainEntryId, (e) => e?.feedId)
|
const feedId = useEntry(mainEntryId, (e) => e?.feedId)
|
||||||
|
|
||||||
const entryIds = useEntryIdsByFeedId(feedId!)
|
const entryIds = useEntryIdsByFeedId(feedId!)
|
||||||
|
|
@ -332,7 +365,7 @@ const FeedPickerItem: FC<{
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContextBlock: FC<{ block: AIChatContextBlock }> = ({ block }) => {
|
const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) => {
|
||||||
const blockActions = useChatBlockActions()
|
const blockActions = useChatBlockActions()
|
||||||
|
|
||||||
const getBlockIcon = () => {
|
const getBlockIcon = () => {
|
||||||
|
|
@ -349,6 +382,17 @@ const ContextBlock: FC<{ block: AIChatContextBlock }> = ({ block }) => {
|
||||||
case "selectedText": {
|
case "selectedText": {
|
||||||
return "i-mgc-quill-pen-cute-re"
|
return "i-mgc-quill-pen-cute-re"
|
||||||
}
|
}
|
||||||
|
case "fileAttachment": {
|
||||||
|
const { type, dataUrl, previewUrl } = block.attachment
|
||||||
|
const fileCategory = getFileCategoryFromMimeType(type)
|
||||||
|
|
||||||
|
// Don't show icon for images with thumbnails, as the thumbnail serves as the icon
|
||||||
|
if (fileCategory === "image" && (dataUrl || previewUrl)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFileIconName(fileCategory)
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return "i-mgc-paper-cute-fi"
|
return "i-mgc-paper-cute-fi"
|
||||||
|
|
@ -368,8 +412,80 @@ const ContextBlock: FC<{ block: AIChatContextBlock }> = ({ block }) => {
|
||||||
case "selectedText": {
|
case "selectedText": {
|
||||||
return `"${block.value}"`
|
return `"${block.value}"`
|
||||||
}
|
}
|
||||||
|
case "fileAttachment": {
|
||||||
|
const { type, name, dataUrl, previewUrl, uploadStatus, errorMessage, uploadProgress } =
|
||||||
|
block.attachment
|
||||||
|
const fileCategory = getFileCategoryFromMimeType(type)
|
||||||
|
|
||||||
|
if (fileCategory === "image" && (dataUrl || previewUrl)) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="relative">
|
||||||
|
<ImageThumbnail
|
||||||
|
previewUrl={previewUrl || dataUrl}
|
||||||
|
originalUrl={dataUrl}
|
||||||
|
alt={name}
|
||||||
|
filename={name}
|
||||||
|
className={"m-0.5 size-5 rounded-md"}
|
||||||
|
/>
|
||||||
|
{uploadStatus === "uploading" && uploadProgress !== undefined && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-black/50">
|
||||||
|
<CircularProgress
|
||||||
|
progress={uploadProgress}
|
||||||
|
size={16}
|
||||||
|
strokeWidth={2}
|
||||||
|
variant="default"
|
||||||
|
className="text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadStatus === "error" && (
|
||||||
|
<div
|
||||||
|
className="bg-red/80 absolute inset-0 flex items-center justify-center rounded-md"
|
||||||
|
title={errorMessage}
|
||||||
|
>
|
||||||
|
<i className="i-mgc-close-cute-re size-3 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<span className="truncate">{name}</span>
|
||||||
|
{uploadStatus === "uploading" && uploadProgress !== undefined && (
|
||||||
|
<div className="text-text-tertiary text-xs">
|
||||||
|
Uploading {Math.round(uploadProgress)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadStatus === "error" && <div className="text-red text-xs">Upload failed</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-image files
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="min-w-0 flex-1 truncate">{name}</span>
|
||||||
|
{uploadStatus === "uploading" && uploadProgress !== undefined && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<CircularProgress
|
||||||
|
progress={uploadProgress}
|
||||||
|
size={14}
|
||||||
|
strokeWidth={2}
|
||||||
|
variant="default"
|
||||||
|
/>
|
||||||
|
<span className="text-text-tertiary text-xs">{Math.round(uploadProgress)}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadStatus === "error" && (
|
||||||
|
<i className="i-mgc-close-cute-re text-red size-3" title={errorMessage} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return block.value
|
// This should never happen with proper discriminated union
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -388,6 +504,9 @@ const ContextBlock: FC<{ block: AIChatContextBlock }> = ({ block }) => {
|
||||||
case "selectedText": {
|
case "selectedText": {
|
||||||
return "Text"
|
return "Text"
|
||||||
}
|
}
|
||||||
|
case "fileAttachment": {
|
||||||
|
return "File"
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -398,30 +517,43 @@ const ContextBlock: FC<{ block: AIChatContextBlock }> = ({ block }) => {
|
||||||
const canRemove = block.type !== "mainEntry"
|
const canRemove = block.type !== "mainEntry"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-fill-tertiary border-border group relative flex h-7 max-w-[calc(50%-0.5rem)] flex-shrink-0 items-center gap-2 rounded-lg border px-2.5">
|
<div
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
className={cn(
|
||||||
<div className="flex items-center gap-1">
|
"group relative flex h-7 max-w-[calc(50%-0.5rem)] flex-shrink-0 items-center gap-2 overflow-hidden rounded-lg px-2.5",
|
||||||
<i className={cn("size-3.5 flex-shrink-0", getBlockIcon())} />
|
"bg-fill-tertiary border-border border",
|
||||||
<span className="text-text-tertiary text-xs font-medium">{getBlockLabel()}</span>
|
)}
|
||||||
</div>
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
canRemove
|
||||||
|
? "group-hover:[mask-image:linear-gradient(to_right,black_0%,black_calc(100%-3rem),rgba(0,0,0,0.8)_calc(100%-2rem),rgba(0,0,0,0.3)_calc(100%-1rem),transparent_100%)]"
|
||||||
|
: void 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{getBlockIcon() && <i className={cn("size-3.5 flex-shrink-0", getBlockIcon())} />}
|
||||||
|
<span className="text-text-tertiary text-xs font-medium">{getBlockLabel()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className={cn("text-text min-w-0 flex-1 truncate text-xs")}>
|
<span className={cn("text-text min-w-0 flex-1 truncate text-xs")}>
|
||||||
{getDisplayContent()}
|
{getDisplayContent()}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canRemove && (
|
{canRemove && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => blockActions.removeBlock(block.id)}
|
onClick={() => blockActions.removeBlock(block.id)}
|
||||||
className="text-text-tertiary hover:text-text-secondary flex-shrink-0 opacity-0 transition-all group-hover:opacity-100"
|
className="text-text/90 cursor-button hover:text-text absolute inset-y-0 right-2 flex-shrink-0 opacity-0 transition-all ease-in group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<i className="i-mgc-close-cute-re size-3" />
|
<i className="i-mgc-close-cute-re size-3" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const EntryTitle: FC<{ entryId?: string; fallback: string }> = ({ entryId, fallback }) => {
|
const EntryTitle: FC<{ entryId?: string; fallback: string }> = ({ entryId, fallback }) => {
|
||||||
const entryTitle = useEntry(entryId!, (e) => e?.title)
|
const entryTitle = useEntry(entryId!, (e) => e?.title)
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,13 @@ export const ChatInput = memo(({ onSend, variant }: ChatInputProps) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(chatInputVariants({ variant }))}>
|
<div
|
||||||
|
className={cn(chatInputVariants({ variant }))}
|
||||||
|
onDragEnter={stopPropagation}
|
||||||
|
onDragOver={stopPropagation}
|
||||||
|
onDragLeave={stopPropagation}
|
||||||
|
onDrop={stopPropagation}
|
||||||
|
>
|
||||||
{/* Input Area */}
|
{/* Input Area */}
|
||||||
<div className="relative z-10 flex items-end" onContextMenu={stopPropagation}>
|
<div className="relative z-10 flex items-end" onContextMenu={stopPropagation}>
|
||||||
<ScrollArea rootClassName="mx-5 my-3.5 mr-14 flex-1 overflow-auto">
|
<ScrollArea rootClassName="mx-5 my-3.5 mr-14 flex-1 overflow-auto">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type { EditorState, LexicalEditor } from "lexical"
|
||||||
import { AnimatePresence } from "motion/react"
|
import { AnimatePresence } from "motion/react"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import type { FC } from "react"
|
import type { FC } from "react"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { Suspense, useCallback, useEffect, useState } from "react"
|
||||||
import { useEventCallback } from "usehooks-ts"
|
import { useEventCallback } from "usehooks-ts"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
} from "~/modules/ai-chat/store/hooks"
|
} from "~/modules/ai-chat/store/hooks"
|
||||||
|
|
||||||
import { convertLexicalToMarkdown } from "../../utils/lexical-markdown"
|
import { convertLexicalToMarkdown } from "../../utils/lexical-markdown"
|
||||||
|
import { GlobalFileDropZone } from "../file/GlobalFileDropZone"
|
||||||
import { AIErrorFallback } from "./AIErrorFallback"
|
import { AIErrorFallback } from "./AIErrorFallback"
|
||||||
import { ChatInput } from "./ChatInput"
|
import { ChatInput } from "./ChatInput"
|
||||||
import { CollapsibleError } from "./CollapsibleError"
|
import { CollapsibleError } from "./CollapsibleError"
|
||||||
|
|
@ -102,13 +103,29 @@ const ChatInterfaceContent = () => {
|
||||||
(message: string | EditorState, editor: LexicalEditor | null) => {
|
(message: string | EditorState, editor: LexicalEditor | null) => {
|
||||||
resetScrollState()
|
resetScrollState()
|
||||||
|
|
||||||
|
const blocks = [] as any[]
|
||||||
|
|
||||||
|
for (const block of blockActions.getBlocks()) {
|
||||||
|
if (block.type === "fileAttachment" && block.attachment.serverUrl) {
|
||||||
|
blocks.push({
|
||||||
|
...block,
|
||||||
|
attachment: {
|
||||||
|
id: block.attachment.id,
|
||||||
|
name: block.attachment.name,
|
||||||
|
type: block.attachment.type,
|
||||||
|
size: block.attachment.size,
|
||||||
|
serverUrl: block.attachment.serverUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
blocks.push(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parts: BizUIMessage["parts"] = [
|
const parts: BizUIMessage["parts"] = [
|
||||||
{
|
{
|
||||||
type: "data-block",
|
type: "data-block",
|
||||||
data: blockActions.getBlocks().map((b) => ({
|
data: blocks,
|
||||||
type: b.type,
|
|
||||||
value: b.value,
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -145,7 +162,7 @@ const ChatInterfaceContent = () => {
|
||||||
const shouldShowScrollToBottom = hasMessages && !isAtBottom && !isLoadingHistory
|
const shouldShowScrollToBottom = hasMessages && !isAtBottom && !isLoadingHistory
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col">
|
<GlobalFileDropZone className="flex size-full flex-col">
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{!hasMessages && !isLoadingHistory ? (
|
{!hasMessages && !isLoadingHistory ? (
|
||||||
|
|
@ -203,7 +220,7 @@ const ChatInterfaceContent = () => {
|
||||||
{error && <CollapsibleError error={error} />}
|
{error && <CollapsibleError error={error} />}
|
||||||
<ChatInput onSend={handleSendMessage} variant={!hasMessages ? "minimal" : "default"} />
|
<ChatInput onSend={handleSendMessage} variant={!hasMessages ? "minimal" : "default"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GlobalFileDropZone>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,5 +233,9 @@ export const ChatInterface = () => (
|
||||||
const Messages: FC = () => {
|
const Messages: FC = () => {
|
||||||
const messages = useMessages()
|
const messages = useMessages()
|
||||||
|
|
||||||
return messages.map((message) => <AIChatMessage key={message.id} message={message} />)
|
return messages.map((message) => (
|
||||||
|
<Suspense key={message.id}>
|
||||||
|
<AIChatMessage message={message} />
|
||||||
|
</Suspense>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { cn } from "@follow/utils"
|
||||||
|
import * as HoverCard from "@radix-ui/react-hover-card"
|
||||||
|
import { m } from "motion/react"
|
||||||
|
import type { FC } from "react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||||
|
|
||||||
|
export const ImageThumbnail: FC<{
|
||||||
|
previewUrl: string
|
||||||
|
originalUrl: string
|
||||||
|
alt: string
|
||||||
|
filename: string
|
||||||
|
className?: string
|
||||||
|
}> = ({ previewUrl, originalUrl, alt, filename, className }) => {
|
||||||
|
const [imageError, setImageError] = React.useState(false)
|
||||||
|
const { present } = useModalStack()
|
||||||
|
return (
|
||||||
|
<HoverCard.Root openDelay={300} closeDelay={100}>
|
||||||
|
<HoverCard.Trigger
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
present({
|
||||||
|
max: true,
|
||||||
|
title: "Preview Image",
|
||||||
|
content: () => (
|
||||||
|
<div className="flex max-h-full max-w-full items-center justify-center">
|
||||||
|
<img src={originalUrl} className="max-h-full max-w-full" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ImageThumbnailInner className={className} src={previewUrl} alt={alt} />
|
||||||
|
</HoverCard.Trigger>
|
||||||
|
<HoverCard.Portal>
|
||||||
|
<HoverCard.Content
|
||||||
|
className="bg-material-thick border-border w-fit rounded-md border shadow-lg"
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden rounded-md">
|
||||||
|
{imageError ? (
|
||||||
|
<div className="bg-fill-secondary border-border flex h-32 w-40 items-center justify-center rounded-md border">
|
||||||
|
<div className="text-text-tertiary flex flex-col items-center gap-2">
|
||||||
|
<i className="i-mgc-photo-album-cute-fi size-6" />
|
||||||
|
<span className="text-xs">{filename}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<m.img
|
||||||
|
src={previewUrl}
|
||||||
|
alt={alt}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="max-h-[300px] max-w-[400px] rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</HoverCard.Content>
|
||||||
|
</HoverCard.Portal>
|
||||||
|
</HoverCard.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageThumbnailInner: React.FC<{
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
className?: string
|
||||||
|
}> = ({ src, alt, className }) => {
|
||||||
|
const [imageError, setImageError] = React.useState(false)
|
||||||
|
const [imageLoaded, setImageLoaded] = React.useState(false)
|
||||||
|
|
||||||
|
const handleError = React.useCallback(() => {
|
||||||
|
setImageError(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLoad = React.useCallback(() => {
|
||||||
|
setImageLoaded(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (imageError) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-fill-secondary border-border flex items-center justify-center border",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<i className="i-mgc-photo-album-cute-re text-text-tertiary size-3" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative overflow-hidden", className)}>
|
||||||
|
{!imageLoaded && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"bg-fill-secondary border-border absolute inset-0 flex items-center justify-center border"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="i-mgc-loading-3-cute-re text-text-tertiary size-3 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<m.img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
onError={handleError}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: imageLoaded ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className={"size-full object-cover"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createDefaultLexicalEditor } from "@follow/components/ui/lexical-rich-editor/editor.js"
|
import { createDefaultLexicalEditor } from "@follow/components/ui/lexical-rich-editor/editor.js"
|
||||||
import { stopPropagation } from "@follow/utils"
|
import { stopPropagation, thenable } from "@follow/utils"
|
||||||
import type { UIDataTypes, UIMessage } from "ai"
|
import type { UIDataTypes, UIMessage } from "ai"
|
||||||
import type { LexicalEditor, SerializedEditorState } from "lexical"
|
import type { LexicalEditor, SerializedEditorState } from "lexical"
|
||||||
import { m } from "motion/react"
|
import { m } from "motion/react"
|
||||||
|
|
@ -31,6 +31,9 @@ interface AIChatMessageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message }) => {
|
export const AIChatMessage: React.FC<AIChatMessageProps> = React.memo(({ message }) => {
|
||||||
|
if (message.parts.length === 0) {
|
||||||
|
throw thenable
|
||||||
|
}
|
||||||
const chatActions = useChatActions()
|
const chatActions = useChatActions()
|
||||||
|
|
||||||
const messageId = message.id
|
const messageId = message.id
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { cn } from "@follow/utils/utils"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
|
||||||
|
|
||||||
|
import {
|
||||||
|
getBlockIcon,
|
||||||
|
getBlockLabel,
|
||||||
|
getBlockStyles,
|
||||||
|
getFileDisplayContent,
|
||||||
|
isImageAttachment,
|
||||||
|
} from "./ai-block-constants"
|
||||||
|
import { EntryTitle, FeedTitle } from "./BlockTitleComponents"
|
||||||
|
import { ImageThumbnail } from "./ImageThumbnail"
|
||||||
|
|
||||||
|
interface AIDataBlockItemProps {
|
||||||
|
block: AIChatContextBlock
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the display content for a context block
|
||||||
|
*/
|
||||||
|
const getDisplayContent = (block: AIChatContextBlock): React.ReactNode => {
|
||||||
|
switch (block.type) {
|
||||||
|
case "mainEntry":
|
||||||
|
case "referEntry": {
|
||||||
|
return <EntryTitle entryId={block.value} fallback={block.value} />
|
||||||
|
}
|
||||||
|
case "referFeed": {
|
||||||
|
return <FeedTitle feedId={block.value} fallback={block.value} />
|
||||||
|
}
|
||||||
|
case "selectedText": {
|
||||||
|
return `"${block.value}"`
|
||||||
|
}
|
||||||
|
case "fileAttachment": {
|
||||||
|
if (!block.attachment) {
|
||||||
|
return "[File: Unknown]"
|
||||||
|
}
|
||||||
|
return getFileDisplayContent(block.attachment)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the appropriate icon or image thumbnail for a block
|
||||||
|
*/
|
||||||
|
const BlockIcon: React.FC<{
|
||||||
|
block: AIChatContextBlock
|
||||||
|
styles: ReturnType<typeof getBlockStyles>
|
||||||
|
}> = React.memo(({ block, styles }) => {
|
||||||
|
// Handle image thumbnails for file attachments
|
||||||
|
if (block.type === "fileAttachment" && block.attachment && isImageAttachment(block)) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex size-5 flex-shrink-0 items-center justify-center rounded-md",
|
||||||
|
styles.icon,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ImageThumbnail attachment={block.attachment} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconClass = getBlockIcon(block)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex size-5 flex-shrink-0 items-center justify-center rounded-md",
|
||||||
|
styles.icon,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<i className={cn("size-3", iconClass)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
BlockIcon.displayName = "BlockIcon"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual block item component with optimized rendering and animations
|
||||||
|
*/
|
||||||
|
export const AIDataBlockItem: React.FC<AIDataBlockItemProps> = React.memo(({ block }) => {
|
||||||
|
const styles = React.useMemo(() => getBlockStyles(block.type), [block.type])
|
||||||
|
const label = React.useMemo(() => getBlockLabel(block.type), [block.type])
|
||||||
|
const displayContent = React.useMemo(() => getDisplayContent(block), [block])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={block.id}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5",
|
||||||
|
"border bg-gradient-to-r backdrop-blur-sm transition-all duration-200",
|
||||||
|
"hover:scale-105 hover:shadow-md",
|
||||||
|
styles.container,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BlockIcon block={block} styles={styles} />
|
||||||
|
|
||||||
|
{/* Label and content */}
|
||||||
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
|
<span className={cn("text-xs font-medium", styles.label)}>{label}</span>
|
||||||
|
<span className="text-text-secondary text-xs">·</span>
|
||||||
|
<span
|
||||||
|
className="text-text max-w-32 truncate text-xs font-medium"
|
||||||
|
title={typeof displayContent === "string" ? displayContent : undefined}
|
||||||
|
>
|
||||||
|
{displayContent}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
AIDataBlockItem.displayName = "AIDataBlockItem"
|
||||||
|
|
@ -1,136 +1,21 @@
|
||||||
import { useEntry } from "@follow/store/entry/hooks"
|
|
||||||
import { useFeedById } from "@follow/store/feed/hooks"
|
|
||||||
import { cn } from "@follow/utils/utils"
|
import { cn } from "@follow/utils/utils"
|
||||||
import { m } from "motion/react"
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
|
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
|
||||||
|
|
||||||
|
import { AIDataBlockItem } from "./AIDataBlockItem"
|
||||||
|
|
||||||
interface AIDataBlockPartProps {
|
interface AIDataBlockPartProps {
|
||||||
blocks: AIChatContextBlock[]
|
blocks: AIChatContextBlock[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper components for displaying titles
|
/**
|
||||||
const EntryTitle: React.FC<{ entryId?: string; fallback: string }> = ({ entryId, fallback }) => {
|
* Main component for rendering AI chat context blocks
|
||||||
const entryTitle = useEntry(entryId!, (e) => e?.title)
|
* Displays various types of context (entries, feeds, text, files) with animations
|
||||||
|
*/
|
||||||
if (!entryId || !entryTitle) {
|
|
||||||
return <span className="text-text-tertiary">{fallback}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span>{entryTitle}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
const FeedTitle: React.FC<{ feedId?: string; fallback: string }> = ({ feedId, fallback }) => {
|
|
||||||
const feed = useFeedById(feedId, (feed) => ({ title: feed?.title }))
|
|
||||||
if (!feedId || !feed) {
|
|
||||||
return <span className="text-text-tertiary">{fallback}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span>{feed.title}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data Block Component
|
|
||||||
export const AIDataBlockPart: React.FC<AIDataBlockPartProps> = React.memo(({ blocks }) => {
|
export const AIDataBlockPart: React.FC<AIDataBlockPartProps> = React.memo(({ blocks }) => {
|
||||||
const getBlockIcon = (type: AIChatContextBlock["type"]) => {
|
// Early return for empty blocks
|
||||||
switch (type) {
|
if (!blocks?.length) {
|
||||||
case "mainEntry": {
|
|
||||||
return "i-mgc-star-cute-fi"
|
|
||||||
}
|
|
||||||
case "referEntry": {
|
|
||||||
return "i-mgc-paper-cute-fi"
|
|
||||||
}
|
|
||||||
case "referFeed": {
|
|
||||||
return "i-mgc-rss-cute-fi"
|
|
||||||
}
|
|
||||||
case "selectedText": {
|
|
||||||
return "i-mgc-quill-pen-cute-re"
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return "i-mgc-paper-cute-fi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBlockLabel = (type: AIChatContextBlock["type"]) => {
|
|
||||||
switch (type) {
|
|
||||||
case "mainEntry": {
|
|
||||||
return "Current"
|
|
||||||
}
|
|
||||||
case "referEntry": {
|
|
||||||
return "Ref"
|
|
||||||
}
|
|
||||||
case "referFeed": {
|
|
||||||
return "Feed"
|
|
||||||
}
|
|
||||||
case "selectedText": {
|
|
||||||
return "Text"
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBlockStyles = (type: AIChatContextBlock["type"]) => {
|
|
||||||
switch (type) {
|
|
||||||
case "mainEntry": {
|
|
||||||
return {
|
|
||||||
container: "from-orange/5 to-orange/10 border-orange/20 hover:border-orange/30",
|
|
||||||
icon: "bg-orange/10 text-orange",
|
|
||||||
label: "text-orange",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "referEntry": {
|
|
||||||
return {
|
|
||||||
container: "from-blue/5 to-blue/10 border-blue/20 hover:border-blue/30",
|
|
||||||
icon: "bg-blue/10 text-blue",
|
|
||||||
label: "text-blue",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "referFeed": {
|
|
||||||
return {
|
|
||||||
container: "from-green/5 to-green/10 border-green/20 hover:border-green/30",
|
|
||||||
icon: "bg-green/10 text-green",
|
|
||||||
label: "text-green",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "selectedText": {
|
|
||||||
return {
|
|
||||||
container: "from-purple/5 to-purple/10 border-purple/20 hover:border-purple/30",
|
|
||||||
icon: "bg-purple/10 text-purple",
|
|
||||||
label: "text-purple",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return {
|
|
||||||
container: "from-gray/5 to-gray/10 border-gray/20 hover:border-gray/30",
|
|
||||||
icon: "bg-gray/10 text-gray",
|
|
||||||
label: "text-gray",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDisplayContent = (block: AIChatContextBlock) => {
|
|
||||||
switch (block.type) {
|
|
||||||
case "mainEntry":
|
|
||||||
case "referEntry": {
|
|
||||||
return <EntryTitle entryId={block.value} fallback={block.value} />
|
|
||||||
}
|
|
||||||
case "referFeed": {
|
|
||||||
return <FeedTitle feedId={block.value} fallback={block.value} />
|
|
||||||
}
|
|
||||||
case "selectedText": {
|
|
||||||
return `"${block.value}"`
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return block.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!blocks || blocks.length === 0) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,50 +33,10 @@ export const AIDataBlockPart: React.FC<AIDataBlockPartProps> = React.memo(({ blo
|
||||||
<span className="text-xs font-medium">Context:</span>
|
<span className="text-xs font-medium">Context:</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Blocks */}
|
{/* Render individual block items */}
|
||||||
{blocks.map((block, index) => {
|
{blocks.map((block, index) => (
|
||||||
const styles = getBlockStyles(block.type)
|
<AIDataBlockItem key={block.id} block={block} index={index} />
|
||||||
|
))}
|
||||||
return (
|
|
||||||
<m.div
|
|
||||||
key={block.id}
|
|
||||||
initial={{ opacity: 0, x: -10 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.3,
|
|
||||||
delay: index * 0.05,
|
|
||||||
ease: [0.25, 0.1, 0.25, 1],
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5",
|
|
||||||
"border bg-gradient-to-r backdrop-blur-sm transition-all duration-200",
|
|
||||||
"hover:scale-105 hover:shadow-md",
|
|
||||||
styles.container,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Icon */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex size-5 flex-shrink-0 items-center justify-center rounded-md",
|
|
||||||
styles.icon,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<i className={cn("size-3", getBlockIcon(block.type))} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Label and content */}
|
|
||||||
<div className="flex min-w-0 items-center gap-1">
|
|
||||||
<span className={cn("text-xs font-medium", styles.label)}>
|
|
||||||
{getBlockLabel(block.type)}
|
|
||||||
</span>
|
|
||||||
<span className="text-text-secondary text-xs">·</span>
|
|
||||||
<span className="text-text max-w-32 truncate text-xs font-medium">
|
|
||||||
{getDisplayContent(block)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</m.div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import "@xyflow/react/dist/style.css"
|
||||||
|
|
||||||
import type { ToolUIPart } from "ai"
|
import type { ToolUIPart } from "ai"
|
||||||
import type { SerializedEditorState } from "lexical"
|
import type { SerializedEditorState } from "lexical"
|
||||||
import { m } from "motion/react"
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -34,30 +33,8 @@ const LazyAIDisplayFlowPart = React.lazy(() =>
|
||||||
interface MessagePartsProps {
|
interface MessagePartsProps {
|
||||||
message: BizUIMessage
|
message: BizUIMessage
|
||||||
}
|
}
|
||||||
const ThinkingIndicator: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-24 items-center">
|
|
||||||
<m.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="relative inline-block"
|
|
||||||
>
|
|
||||||
<span className="from-text-tertiary via-text to-text-tertiary animate-[shimmer_5s_linear_infinite] bg-gradient-to-r bg-[length:200%_100%] bg-clip-text text-sm font-medium text-transparent">
|
|
||||||
Thinking...
|
|
||||||
</span>
|
|
||||||
</m.div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AIMessageParts: React.FC<MessagePartsProps> = React.memo(({ message }) => {
|
export const AIMessageParts: React.FC<MessagePartsProps> = React.memo(({ message }) => {
|
||||||
if (!message.parts || message.parts.length === 0) {
|
|
||||||
// In AI SDK v5, messages should always have parts
|
|
||||||
if (message.role === "assistant") {
|
|
||||||
return <ThinkingIndicator />
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const isUser = message.role === "user"
|
const isUser = message.role === "user"
|
||||||
|
|
||||||
return message.parts.map((part, index) => {
|
return message.parts.map((part, index) => {
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,17 @@ interface AIRichTextMessageProps {
|
||||||
|
|
||||||
export const AIRichTextMessage: React.FC<AIRichTextMessageProps> = React.memo(
|
export const AIRichTextMessage: React.FC<AIRichTextMessageProps> = React.memo(
|
||||||
({ data, className }) => {
|
({ data, className }) => {
|
||||||
const initialConfig: InitialConfigType = {
|
let initialConfig: InitialConfigType = null!
|
||||||
namespace: "AIRichTextDisplay",
|
if (!initialConfig) {
|
||||||
theme: defaultLexicalTheme,
|
initialConfig = {
|
||||||
onError,
|
namespace: "AIRichTextDisplay",
|
||||||
editable: false, // Read-only mode
|
theme: defaultLexicalTheme,
|
||||||
editorState: JSON.stringify(data.state),
|
onError,
|
||||||
nodes: [...LexicalRichEditorNodes, MentionNode],
|
editable: false, // Read-only mode
|
||||||
|
editorState: JSON.stringify(data.state),
|
||||||
|
nodes: [...LexicalRichEditorNodes, MentionNode],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("text-text relative text-sm", className)}>
|
<div className={cn("text-text relative text-sm", className)}>
|
||||||
<LexicalComposer initialConfig={initialConfig}>
|
<LexicalComposer initialConfig={initialConfig}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useEntry } from "@follow/store/entry/hooks"
|
||||||
|
import { useFeedById } from "@follow/store/feed/hooks"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
interface TitleProps {
|
||||||
|
fallback: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryTitleProps extends TitleProps {
|
||||||
|
entryId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedTitleProps extends TitleProps {
|
||||||
|
feedId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays entry title with fallback handling
|
||||||
|
*/
|
||||||
|
export const EntryTitle: React.FC<EntryTitleProps> = React.memo(({ entryId, fallback }) => {
|
||||||
|
const entryTitle = useEntry(entryId!, (e) => e?.title)
|
||||||
|
|
||||||
|
if (!entryId || !entryTitle) {
|
||||||
|
return <span className="text-text-tertiary">{fallback}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span title={entryTitle}>{entryTitle}</span>
|
||||||
|
})
|
||||||
|
|
||||||
|
EntryTitle.displayName = "EntryTitle"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays feed title with fallback handling
|
||||||
|
*/
|
||||||
|
export const FeedTitle: React.FC<FeedTitleProps> = React.memo(({ feedId, fallback }) => {
|
||||||
|
const feed = useFeedById(feedId, (feed) => ({ title: feed?.title }))
|
||||||
|
|
||||||
|
if (!feedId || !feed?.title) {
|
||||||
|
return <span className="text-text-tertiary">{fallback}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span title={feed.title}>{feed.title}</span>
|
||||||
|
})
|
||||||
|
|
||||||
|
FeedTitle.displayName = "FeedTitle"
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { cn } from "@follow/utils/utils"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type { FileAttachment } from "~/modules/ai-chat/store/types"
|
||||||
|
|
||||||
|
import { getImageUrl } from "./ai-block-constants"
|
||||||
|
|
||||||
|
interface ImageThumbnailProps {
|
||||||
|
attachment: FileAttachment
|
||||||
|
className?: string
|
||||||
|
fallbackIcon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A robust image thumbnail component with proper error handling and fallback states
|
||||||
|
*/
|
||||||
|
export const ImageThumbnail: React.FC<ImageThumbnailProps> = React.memo(
|
||||||
|
({
|
||||||
|
attachment,
|
||||||
|
className = "size-3 rounded object-cover",
|
||||||
|
fallbackIcon = "i-mgc-pic-cute-re",
|
||||||
|
}) => {
|
||||||
|
const [hasError, setHasError] = React.useState(false)
|
||||||
|
const [isLoading, setIsLoading] = React.useState(true)
|
||||||
|
|
||||||
|
const imageUrl = React.useMemo(() => getImageUrl(attachment), [attachment])
|
||||||
|
|
||||||
|
const handleError = React.useCallback(() => {
|
||||||
|
setHasError(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLoad = React.useCallback(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Reset error state when imageUrl changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (imageUrl) {
|
||||||
|
setHasError(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
}
|
||||||
|
}, [imageUrl])
|
||||||
|
|
||||||
|
// If no image URL or there was an error, show fallback icon
|
||||||
|
if (!imageUrl || hasError) {
|
||||||
|
return <i className={cn("size-3", fallbackIcon)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={attachment.name}
|
||||||
|
className={cn(className, isLoading && "opacity-50")}
|
||||||
|
onError={handleError}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<i className="i-mgc-loading-3-cute-re size-2 animate-spin opacity-50" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ImageThumbnail.displayName = "ImageThumbnail"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { m } from "motion/react"
|
||||||
|
|
||||||
|
export const ThinkingIndicator: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-24 items-center">
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="relative inline-block"
|
||||||
|
>
|
||||||
|
<span className="from-text-tertiary via-text to-text-tertiary animate-[shimmer_5s_linear_infinite] bg-gradient-to-r bg-[length:200%_100%] bg-clip-text text-sm font-medium text-transparent">
|
||||||
|
Thinking...
|
||||||
|
</span>
|
||||||
|
</m.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import type { AIChatContextBlock, FileAttachment } from "~/modules/ai-chat/store/types"
|
||||||
|
import {
|
||||||
|
getFileCategoryFromMimeType,
|
||||||
|
getFileIconName,
|
||||||
|
} from "~/modules/ai-chat/utils/file-validation"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block style configurations for different context block types
|
||||||
|
*/
|
||||||
|
export const BLOCK_STYLES = {
|
||||||
|
mainEntry: {
|
||||||
|
container: "from-orange/5 to-orange/10 border-orange/20 hover:border-orange/30",
|
||||||
|
icon: "bg-orange/10 text-orange",
|
||||||
|
label: "text-orange",
|
||||||
|
},
|
||||||
|
referEntry: {
|
||||||
|
container: "from-blue/5 to-blue/10 border-blue/20 hover:border-blue/30",
|
||||||
|
icon: "bg-blue/10 text-blue",
|
||||||
|
label: "text-blue",
|
||||||
|
},
|
||||||
|
referFeed: {
|
||||||
|
container: "from-green/5 to-green/10 border-green/20 hover:border-green/30",
|
||||||
|
icon: "bg-green/10 text-green",
|
||||||
|
label: "text-green",
|
||||||
|
},
|
||||||
|
selectedText: {
|
||||||
|
container: "from-purple/5 to-purple/10 border-purple/20 hover:border-purple/30",
|
||||||
|
icon: "bg-purple/10 text-purple",
|
||||||
|
label: "text-purple",
|
||||||
|
},
|
||||||
|
fileAttachment: {
|
||||||
|
container: "from-pink/5 to-pink/10 border-pink/20 hover:border-pink/30",
|
||||||
|
icon: "bg-pink/10 text-pink",
|
||||||
|
label: "text-pink",
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default fallback styles for unknown block types
|
||||||
|
*/
|
||||||
|
export const DEFAULT_BLOCK_STYLES = {
|
||||||
|
container: "from-gray/5 to-gray/10 border-gray/20 hover:border-gray/30",
|
||||||
|
icon: "bg-gray/10 text-gray",
|
||||||
|
label: "text-gray",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block icons for different context block types
|
||||||
|
*/
|
||||||
|
export const BLOCK_ICONS = {
|
||||||
|
mainEntry: "i-mgc-star-cute-fi",
|
||||||
|
referEntry: "i-mgc-paper-cute-fi",
|
||||||
|
referFeed: "i-mgc-rss-cute-fi",
|
||||||
|
selectedText: "i-mgc-quill-pen-cute-re",
|
||||||
|
fileAttachment: "i-mgc-file-upload-cute-re",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block labels for different context block types
|
||||||
|
*/
|
||||||
|
export const BLOCK_LABELS = {
|
||||||
|
mainEntry: "Current",
|
||||||
|
referEntry: "Ref",
|
||||||
|
referFeed: "Feed",
|
||||||
|
selectedText: "Text",
|
||||||
|
fileAttachment: "File",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File upload status labels
|
||||||
|
*/
|
||||||
|
export const FILE_STATUS_LABELS = {
|
||||||
|
uploading: "Uploading...",
|
||||||
|
error: "Failed",
|
||||||
|
processing: "Processing...",
|
||||||
|
completed: "",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate styles for a block type
|
||||||
|
*/
|
||||||
|
export function getBlockStyles(type: AIChatContextBlock["type"]) {
|
||||||
|
return BLOCK_STYLES[type] || DEFAULT_BLOCK_STYLES
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate icon for a block
|
||||||
|
*/
|
||||||
|
export function getBlockIcon(block: AIChatContextBlock): string {
|
||||||
|
if (block.type === "fileAttachment" && block.attachment) {
|
||||||
|
const fileCategory = getFileCategoryFromMimeType(block.attachment.type)
|
||||||
|
return getFileIconName(fileCategory)
|
||||||
|
}
|
||||||
|
return BLOCK_ICONS[block.type] || BLOCK_ICONS.fileAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate label for a block type
|
||||||
|
*/
|
||||||
|
export function getBlockLabel(type: AIChatContextBlock["type"]): string {
|
||||||
|
return BLOCK_LABELS[type] || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the best available image URL for a file attachment
|
||||||
|
*/
|
||||||
|
export function getImageUrl(attachment: FileAttachment): string | null {
|
||||||
|
return attachment.previewUrl || attachment.dataUrl || attachment.serverUrl || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a block represents an image attachment
|
||||||
|
*/
|
||||||
|
export function isImageAttachment(block: AIChatContextBlock): boolean {
|
||||||
|
return (
|
||||||
|
block.type === "fileAttachment" &&
|
||||||
|
!!block.attachment &&
|
||||||
|
getFileCategoryFromMimeType(block.attachment.type) === "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets display content for file attachments based on upload status
|
||||||
|
*/
|
||||||
|
export function getFileDisplayContent(attachment: FileAttachment): string {
|
||||||
|
const statusLabel = FILE_STATUS_LABELS[attachment.uploadStatus]
|
||||||
|
return statusLabel || attachment.name
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { cn } from "@follow/utils"
|
||||||
|
import { m } from "motion/react"
|
||||||
|
import type { FC } from "react"
|
||||||
|
import { memo } from "react"
|
||||||
|
|
||||||
|
export interface UploadProgressProps {
|
||||||
|
/** Progress percentage (0-100) */
|
||||||
|
progress: number
|
||||||
|
/** Progress bar size variant */
|
||||||
|
size?: "sm" | "md" | "lg"
|
||||||
|
/** Show percentage text */
|
||||||
|
showPercentage?: boolean
|
||||||
|
/** Custom className */
|
||||||
|
className?: string
|
||||||
|
/** Progress bar color */
|
||||||
|
variant?: "default" | "success" | "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadProgress: FC<UploadProgressProps> = memo(
|
||||||
|
({ progress, size = "md", showPercentage = false, className, variant = "default" }) => {
|
||||||
|
const progressValue = Math.max(0, Math.min(100, progress))
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: "h-1",
|
||||||
|
md: "h-2",
|
||||||
|
lg: "h-3",
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorClasses = {
|
||||||
|
default: "bg-blue",
|
||||||
|
success: "bg-green",
|
||||||
|
error: "bg-red",
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("w-full", className)}>
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-fill-secondary relative overflow-hidden rounded-full",
|
||||||
|
sizeClasses[size],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<m.div
|
||||||
|
className={cn("h-full rounded-full transition-colors", colorClasses[variant])}
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${progressValue}%` }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 100,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Percentage Text */}
|
||||||
|
{showPercentage && (
|
||||||
|
<div className="text-text-tertiary mt-1 text-center text-xs">
|
||||||
|
{Math.round(progressValue)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
UploadProgress.displayName = "UploadProgress"
|
||||||
|
|
||||||
|
export interface CircularProgressProps {
|
||||||
|
/** Progress percentage (0-100) */
|
||||||
|
progress: number
|
||||||
|
/** Circle size */
|
||||||
|
size?: number
|
||||||
|
/** Stroke width */
|
||||||
|
strokeWidth?: number
|
||||||
|
/** Show percentage text in center */
|
||||||
|
showPercentage?: boolean
|
||||||
|
/** Custom className */
|
||||||
|
className?: string
|
||||||
|
/** Progress color */
|
||||||
|
variant?: "default" | "success" | "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CircularProgress: FC<CircularProgressProps> = memo(
|
||||||
|
({
|
||||||
|
progress,
|
||||||
|
size = 20,
|
||||||
|
strokeWidth = 2,
|
||||||
|
showPercentage = false,
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
}) => {
|
||||||
|
const progressValue = Math.max(0, Math.min(100, progress))
|
||||||
|
const radius = (size - strokeWidth) / 2
|
||||||
|
const circumference = 2 * Math.PI * radius
|
||||||
|
const strokeDashoffset = circumference - (progressValue / 100) * circumference
|
||||||
|
|
||||||
|
const colorClasses = {
|
||||||
|
default: "text-blue",
|
||||||
|
success: "text-green",
|
||||||
|
error: "text-red",
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative inline-flex items-center justify-center", className)}>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className="-rotate-90 transform"
|
||||||
|
viewBox={`0 0 ${size} ${size}`}
|
||||||
|
>
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
className="text-fill-tertiary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Progress circle */}
|
||||||
|
<m.circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
className={colorClasses[variant]}
|
||||||
|
strokeLinecap="round"
|
||||||
|
initial={{ strokeDasharray: circumference, strokeDashoffset: circumference }}
|
||||||
|
animate={{ strokeDashoffset }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 100,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Percentage text */}
|
||||||
|
{showPercentage && (
|
||||||
|
<div className="text-text-tertiary absolute inset-0 flex items-center justify-center text-xs font-medium">
|
||||||
|
{Math.round(progressValue)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
CircularProgress.displayName = "CircularProgress"
|
||||||
|
|
@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef } from "react"
|
||||||
|
|
||||||
import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext"
|
import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext"
|
||||||
import { useChatBlockActions } from "~/modules/ai-chat/store/hooks"
|
import { useChatBlockActions } from "~/modules/ai-chat/store/hooks"
|
||||||
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
|
import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types"
|
||||||
|
|
||||||
import { $isMentionNode, MentionNode } from "../MentionNode"
|
import { $isMentionNode, MentionNode } from "../MentionNode"
|
||||||
import type { MentionData } from "../types"
|
import type { MentionData } from "../types"
|
||||||
|
|
@ -18,7 +18,7 @@ interface MentionBlockReference {
|
||||||
|
|
||||||
const getResourceId = (type: string, value: string) => `${type}:${value}`
|
const getResourceId = (type: string, value: string) => `${type}:${value}`
|
||||||
|
|
||||||
const getBlockType = (mentionType: string): AIChatContextBlock["type"] => {
|
const getBlockType = (mentionType: string): ValueContextBlock["type"] => {
|
||||||
return mentionType === "feed" ? "referFeed" : "referEntry"
|
return mentionType === "feed" ? "referFeed" : "referEntry"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ export const useMentionBlockSync = () => {
|
||||||
const blockType = getBlockType(mentionData.type)
|
const blockType = getBlockType(mentionData.type)
|
||||||
|
|
||||||
// Generate block ID (mimicking the block slice logic)
|
// Generate block ID (mimicking the block slice logic)
|
||||||
const newBlock: Omit<AIChatContextBlock, "id"> = {
|
const newBlock: Omit<ValueContextBlock, "id"> = {
|
||||||
type: blockType,
|
type: blockType,
|
||||||
value: mentionData.value as string,
|
value: mentionData.value as string,
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +115,8 @@ export const useMentionBlockSync = () => {
|
||||||
// Use current blocks state directly from store instead of stale closure
|
// Use current blocks state directly from store instead of stale closure
|
||||||
const currentBlocks = blockActions.getBlocks()
|
const currentBlocks = blockActions.getBlocks()
|
||||||
const addedBlock = currentBlocks.find(
|
const addedBlock = currentBlocks.find(
|
||||||
(block) => block.type === blockType && block.value === mentionData.value,
|
(block): block is Extract<AIChatContextBlock, { type: typeof blockType }> =>
|
||||||
|
block.type === blockType && block.value === mentionData.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (addedBlock) {
|
if (addedBlock) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
import { useChatBlockActions } from "../store/hooks"
|
||||||
|
import type { ProcessFileOptions, ProcessFileResult } from "../utils/file-processing"
|
||||||
|
import { processAndUploadFile } from "../utils/file-processing"
|
||||||
|
|
||||||
|
export interface UseFileUploadOptions extends ProcessFileOptions {
|
||||||
|
/**
|
||||||
|
* Show success toast on successful upload
|
||||||
|
*/
|
||||||
|
showSuccessToast?: boolean
|
||||||
|
/**
|
||||||
|
* Show error toast on upload failure
|
||||||
|
*/
|
||||||
|
showErrorToast?: boolean
|
||||||
|
/**
|
||||||
|
* Custom success message for toast
|
||||||
|
*/
|
||||||
|
successMessage?: string
|
||||||
|
/**
|
||||||
|
* Custom error message prefix for toast
|
||||||
|
*/
|
||||||
|
errorMessagePrefix?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadHandlers {
|
||||||
|
/**
|
||||||
|
* Upload a single file with progress tracking
|
||||||
|
*/
|
||||||
|
uploadFile: (file: File) => Promise<ProcessFileResult>
|
||||||
|
/**
|
||||||
|
* Upload multiple files with progress tracking
|
||||||
|
*/
|
||||||
|
uploadFiles: (files: File[] | FileList) => Promise<ProcessFileResult[]>
|
||||||
|
/**
|
||||||
|
* Handle file input change event
|
||||||
|
*/
|
||||||
|
handleFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>
|
||||||
|
/**
|
||||||
|
* Handle drag and drop files
|
||||||
|
*/
|
||||||
|
handleFileDrop: (files: FileList) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for handling file uploads with progress tracking and block management
|
||||||
|
*/
|
||||||
|
export function useFileUpload(options: UseFileUploadOptions = {}): FileUploadHandlers {
|
||||||
|
const {
|
||||||
|
showSuccessToast = false,
|
||||||
|
showErrorToast = true,
|
||||||
|
successMessage = "File uploaded successfully",
|
||||||
|
errorMessagePrefix = "File upload error",
|
||||||
|
...processOptions
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const blockActions = useChatBlockActions()
|
||||||
|
|
||||||
|
const uploadFile = useCallback(
|
||||||
|
async (file: File): Promise<ProcessFileResult> => {
|
||||||
|
try {
|
||||||
|
const result = await processAndUploadFile(file, processOptions, (updatedAttachment) => {
|
||||||
|
// Update block during upload progress
|
||||||
|
blockActions.updateFileAttachment(updatedAttachment.id, updatedAttachment)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success && result.fileAttachment) {
|
||||||
|
// Add the completed file attachment to blocks
|
||||||
|
blockActions.addFileAttachment(result.fileAttachment)
|
||||||
|
|
||||||
|
if (showSuccessToast) {
|
||||||
|
toast.success(`${successMessage}: ${file.name}`)
|
||||||
|
}
|
||||||
|
} else if (showErrorToast && result.error) {
|
||||||
|
toast.error(`${errorMessagePrefix}: ${result.error}`)
|
||||||
|
console.error("File upload error:", result.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||||
|
|
||||||
|
if (showErrorToast) {
|
||||||
|
toast.error(`${errorMessagePrefix}: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("File upload failed:", error)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
blockActions,
|
||||||
|
processOptions,
|
||||||
|
showSuccessToast,
|
||||||
|
showErrorToast,
|
||||||
|
successMessage,
|
||||||
|
errorMessagePrefix,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
const uploadFiles = useCallback(
|
||||||
|
async (files: File[] | FileList): Promise<ProcessFileResult[]> => {
|
||||||
|
const results: ProcessFileResult[] = []
|
||||||
|
const fileArray = Array.from(files)
|
||||||
|
|
||||||
|
// Process files sequentially to avoid overwhelming the server
|
||||||
|
for (const file of fileArray) {
|
||||||
|
const result = await uploadFile(file)
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
},
|
||||||
|
[uploadFile],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFileInputChange = useCallback(
|
||||||
|
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { files } = event.target
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
await uploadFiles(files)
|
||||||
|
}
|
||||||
|
// Reset file input
|
||||||
|
event.target.value = ""
|
||||||
|
},
|
||||||
|
[uploadFiles],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFileDrop = useCallback(
|
||||||
|
async (files: FileList) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
await uploadFiles(files)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[uploadFiles],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadFile,
|
||||||
|
uploadFiles,
|
||||||
|
handleFileInputChange,
|
||||||
|
handleFileDrop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience hook for file upload with default settings
|
||||||
|
*/
|
||||||
|
export function useFileUploadWithDefaults(): FileUploadHandlers {
|
||||||
|
return useFileUpload({
|
||||||
|
showErrorToast: true,
|
||||||
|
showSuccessToast: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,8 @@ import { produce } from "immer"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import type { StateCreator } from "zustand"
|
import type { StateCreator } from "zustand"
|
||||||
|
|
||||||
import type { AIChatContextBlock } from "../types"
|
import { cleanupFileAttachment } from "../../utils/file-processing"
|
||||||
|
import type { AIChatContextBlock, AIChatContextBlockInput, FileAttachment } from "../types"
|
||||||
|
|
||||||
export interface BlockSlice {
|
export interface BlockSlice {
|
||||||
blocks: AIChatContextBlock[]
|
blocks: AIChatContextBlock[]
|
||||||
|
|
@ -39,7 +40,7 @@ export class BlockSliceAction {
|
||||||
get get() {
|
get get() {
|
||||||
return this.params[1]
|
return this.params[1]
|
||||||
}
|
}
|
||||||
addBlock(block: Omit<AIChatContextBlock, "id">) {
|
addBlock(block: AIChatContextBlockInput) {
|
||||||
const currentBlocks = this.get().blocks
|
const currentBlocks = this.get().blocks
|
||||||
|
|
||||||
// Only allow one SPECIAL_TYPES
|
// Only allow one SPECIAL_TYPES
|
||||||
|
|
@ -60,6 +61,10 @@ export class BlockSliceAction {
|
||||||
removeBlock(id: string) {
|
removeBlock(id: string) {
|
||||||
this.set(
|
this.set(
|
||||||
produce((state: BlockSlice) => {
|
produce((state: BlockSlice) => {
|
||||||
|
const blockToRemove = state.blocks.find((block) => block.id === id)
|
||||||
|
if (blockToRemove && blockToRemove.type === "fileAttachment") {
|
||||||
|
cleanupFileAttachment(blockToRemove.attachment)
|
||||||
|
}
|
||||||
state.blocks = state.blocks.filter((block) => block.id !== id)
|
state.blocks = state.blocks.filter((block) => block.id !== id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -68,9 +73,18 @@ export class BlockSliceAction {
|
||||||
updateBlock(id: string, updates: Partial<AIChatContextBlock>) {
|
updateBlock(id: string, updates: Partial<AIChatContextBlock>) {
|
||||||
this.set(
|
this.set(
|
||||||
produce((state: BlockSlice) => {
|
produce((state: BlockSlice) => {
|
||||||
state.blocks = state.blocks.map((block) =>
|
state.blocks = state.blocks.map((block) => {
|
||||||
block.id === id ? { ...block, ...updates } : block,
|
if (block.id !== id) return block
|
||||||
)
|
|
||||||
|
// Handle discriminated union updates carefully
|
||||||
|
if (updates.type && updates.type !== block.type) {
|
||||||
|
// Type change - need to replace the entire block
|
||||||
|
return { ...updates, id } as AIChatContextBlock
|
||||||
|
} else {
|
||||||
|
// Same type - safe to spread
|
||||||
|
return { ...block, ...updates } as AIChatContextBlock
|
||||||
|
}
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +101,12 @@ export class BlockSliceAction {
|
||||||
clearBlocks() {
|
clearBlocks() {
|
||||||
this.set(
|
this.set(
|
||||||
produce((state: BlockSlice) => {
|
produce((state: BlockSlice) => {
|
||||||
|
// Clean up file attachments before clearing
|
||||||
|
state.blocks.forEach((block) => {
|
||||||
|
if (block.type === "fileAttachment") {
|
||||||
|
cleanupFileAttachment(block.attachment)
|
||||||
|
}
|
||||||
|
})
|
||||||
state.blocks = []
|
state.blocks = []
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -95,6 +115,12 @@ export class BlockSliceAction {
|
||||||
resetContext() {
|
resetContext() {
|
||||||
this.set(
|
this.set(
|
||||||
produce((state: BlockSlice) => {
|
produce((state: BlockSlice) => {
|
||||||
|
// Clean up file attachments before resetting
|
||||||
|
state.blocks.forEach((block) => {
|
||||||
|
if (block.type === "fileAttachment") {
|
||||||
|
cleanupFileAttachment(block.attachment)
|
||||||
|
}
|
||||||
|
})
|
||||||
state.blocks = []
|
state.blocks = []
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -103,4 +129,55 @@ export class BlockSliceAction {
|
||||||
getBlocks() {
|
getBlocks() {
|
||||||
return this.get().blocks
|
return this.get().blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File attachment specific methods
|
||||||
|
addFileAttachment(fileAttachment: FileAttachment) {
|
||||||
|
const fileBlock: AIChatContextBlock = {
|
||||||
|
id: fileAttachment.id,
|
||||||
|
type: "fileAttachment",
|
||||||
|
attachment: fileAttachment,
|
||||||
|
}
|
||||||
|
this.addBlock(fileBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileAttachment(fileId: string, updatedAttachment: FileAttachment) {
|
||||||
|
this.set(
|
||||||
|
produce((state: BlockSlice) => {
|
||||||
|
const block = state.blocks.find((b) => b.id === fileId)
|
||||||
|
if (block && block.type === "fileAttachment") {
|
||||||
|
block.attachment = updatedAttachment
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileAttachmentStatus(
|
||||||
|
fileId: string,
|
||||||
|
status: FileAttachment["uploadStatus"],
|
||||||
|
errorMessage?: string,
|
||||||
|
) {
|
||||||
|
this.set(
|
||||||
|
produce((state: BlockSlice) => {
|
||||||
|
const block = state.blocks.find((b) => b.id === fileId)
|
||||||
|
if (block && block.type === "fileAttachment") {
|
||||||
|
block.attachment.uploadStatus = status
|
||||||
|
if (errorMessage) {
|
||||||
|
block.attachment.errorMessage = errorMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFileAttachment(fileId: string) {
|
||||||
|
this.removeBlock(fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileAttachments() {
|
||||||
|
return this.get().blocks.filter((block) => block.type === "fileAttachment")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFileAttachments() {
|
||||||
|
return this.get().blocks.some((block) => block.type === "fileAttachment")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,40 @@
|
||||||
import type { BizUITools, ToolWithState } from "@folo-services/ai-tools"
|
import type { BizUITools, ToolWithState } from "@folo-services/ai-tools"
|
||||||
|
|
||||||
export interface AIChatContextBlock {
|
export interface FileAttachment {
|
||||||
id: string
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
size: number
|
||||||
|
dataUrl: string
|
||||||
|
previewUrl?: string
|
||||||
|
uploadStatus: "processing" | "uploading" | "completed" | "error"
|
||||||
|
serverUrl?: string
|
||||||
|
errorMessage?: string
|
||||||
|
/** Upload progress percentage (0-100) */
|
||||||
|
uploadProgress?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseContextBlock {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValueContextBlock extends BaseContextBlock {
|
||||||
type: "mainEntry" | "referEntry" | "referFeed" | "selectedText"
|
type: "mainEntry" | "referEntry" | "referFeed" | "selectedText"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileAttachmentContextBlock extends BaseContextBlock {
|
||||||
|
type: "fileAttachment"
|
||||||
|
attachment: FileAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIChatContextBlock = ValueContextBlock | FileAttachmentContextBlock
|
||||||
|
|
||||||
|
// Helper type for creating new blocks without id
|
||||||
|
export type AIChatContextBlockInput =
|
||||||
|
| Omit<ValueContextBlock, "id">
|
||||||
|
| Omit<FileAttachmentContextBlock, "id">
|
||||||
|
|
||||||
export interface AIChatStoreInitial {
|
export interface AIChatStoreInitial {
|
||||||
blocks: AIChatContextBlock[]
|
blocks: AIChatContextBlock[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
|
||||||
|
import { followApi } from "~/lib/api-client"
|
||||||
|
|
||||||
|
import type { FileAttachment } from "../store/types"
|
||||||
|
import type { FileValidationResult } from "./file-validation"
|
||||||
|
import { validateFile } from "./file-validation"
|
||||||
|
|
||||||
|
export interface ProcessFileOptions {
|
||||||
|
maxImageWidth?: number
|
||||||
|
maxImageHeight?: number
|
||||||
|
imageQuality?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessFileResult {
|
||||||
|
success: boolean
|
||||||
|
fileAttachment?: FileAttachment
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processFile(
|
||||||
|
file: File,
|
||||||
|
options: ProcessFileOptions = {},
|
||||||
|
): Promise<ProcessFileResult> {
|
||||||
|
const { maxImageWidth = 1920, maxImageHeight = 1080, imageQuality = 0.85 } = options
|
||||||
|
|
||||||
|
// Validate file
|
||||||
|
const validation: FileValidationResult = validateFile(file)
|
||||||
|
if (!validation.isValid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: validation.error?.message || "File validation failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileId = nanoid()
|
||||||
|
let dataUrl: string
|
||||||
|
let previewUrl: string | undefined
|
||||||
|
|
||||||
|
if (validation.fileInfo?.category === "image") {
|
||||||
|
// Process image: compress and generate preview
|
||||||
|
const processedImage = await processImage(file, {
|
||||||
|
maxWidth: maxImageWidth,
|
||||||
|
maxHeight: maxImageHeight,
|
||||||
|
quality: imageQuality,
|
||||||
|
})
|
||||||
|
|
||||||
|
dataUrl = processedImage.dataUrl
|
||||||
|
previewUrl = processedImage.previewUrl
|
||||||
|
} else {
|
||||||
|
// For non-images, just convert to data URL
|
||||||
|
dataUrl = await fileToDataUrl(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileAttachment: FileAttachment = {
|
||||||
|
id: fileId,
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
dataUrl,
|
||||||
|
previewUrl,
|
||||||
|
uploadStatus: "completed",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
fileAttachment,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to process file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessImageResult {
|
||||||
|
dataUrl: string
|
||||||
|
previewUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processImage(
|
||||||
|
file: File,
|
||||||
|
options: { maxWidth: number; maxHeight: number; quality: number },
|
||||||
|
): Promise<ProcessImageResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
const canvas = document.createElement("canvas")
|
||||||
|
const ctx = canvas.getContext("2d")
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error("Could not get canvas context"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// Calculate new dimensions
|
||||||
|
let { width, height } = img
|
||||||
|
const { maxWidth, maxHeight, quality } = options
|
||||||
|
|
||||||
|
if (width > maxWidth || height > maxHeight) {
|
||||||
|
const ratio = Math.min(maxWidth / width, maxHeight / height)
|
||||||
|
width *= ratio
|
||||||
|
height *= ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas dimensions
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
|
||||||
|
// Draw and compress image
|
||||||
|
ctx.drawImage(img, 0, 0, width, height)
|
||||||
|
|
||||||
|
const dataUrl = canvas.toDataURL(file.type, quality)
|
||||||
|
|
||||||
|
// Create smaller preview (thumbnail)
|
||||||
|
const previewCanvas = document.createElement("canvas")
|
||||||
|
const previewCtx = previewCanvas.getContext("2d")
|
||||||
|
|
||||||
|
if (previewCtx) {
|
||||||
|
const previewSize = 150
|
||||||
|
const previewRatio = Math.min(previewSize / width, previewSize / height)
|
||||||
|
previewCanvas.width = width * previewRatio
|
||||||
|
previewCanvas.height = height * previewRatio
|
||||||
|
|
||||||
|
previewCtx.drawImage(img, 0, 0, previewCanvas.width, previewCanvas.height)
|
||||||
|
const previewUrl = previewCanvas.toDataURL(file.type, 0.7)
|
||||||
|
|
||||||
|
resolve({ dataUrl, previewUrl })
|
||||||
|
} else {
|
||||||
|
resolve({ dataUrl, previewUrl: dataUrl })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error("Failed to load image"))
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = URL.createObjectURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileToDataUrl(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error("Failed to read file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processFileList(
|
||||||
|
files: FileList,
|
||||||
|
options?: ProcessFileOptions,
|
||||||
|
): Promise<ProcessFileResult[]> {
|
||||||
|
const results: ProcessFileResult[] = []
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file) {
|
||||||
|
const result = await processFile(file, options)
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFileAttachmentBlock(fileAttachment: FileAttachment) {
|
||||||
|
return {
|
||||||
|
id: fileAttachment.id,
|
||||||
|
type: "fileAttachment" as const,
|
||||||
|
attachment: fileAttachment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to clean up object URLs to prevent memory leaks
|
||||||
|
export function cleanupFileAttachment(fileAttachment: FileAttachment) {
|
||||||
|
if (fileAttachment.previewUrl?.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(fileAttachment.previewUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFileAttachment(
|
||||||
|
fileAttachment: FileAttachment,
|
||||||
|
onProgressUpdate?: (attachment: FileAttachment) => void,
|
||||||
|
): Promise<FileAttachment> {
|
||||||
|
try {
|
||||||
|
// Update status to uploading with 0% progress
|
||||||
|
let currentAttachment: FileAttachment = {
|
||||||
|
...fileAttachment,
|
||||||
|
uploadStatus: "uploading" as const,
|
||||||
|
uploadProgress: 0,
|
||||||
|
}
|
||||||
|
onProgressUpdate?.(currentAttachment)
|
||||||
|
|
||||||
|
const { dataUrl } = fileAttachment
|
||||||
|
const blob = await fetch(dataUrl).then((r) => r.blob())
|
||||||
|
|
||||||
|
// TODO: Replace with real progress tracking when followApi supports it
|
||||||
|
// Currently followApi.upload.uploadChatAttachment doesn't provide progress callbacks
|
||||||
|
// Future implementation could use XMLHttpRequest or a custom fetch wrapper
|
||||||
|
|
||||||
|
// Simulate realistic progress updates during upload
|
||||||
|
// This mimics a realistic upload progression pattern
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
if (currentAttachment.uploadProgress! < 85) {
|
||||||
|
// Start faster, then slow down (realistic network behavior)
|
||||||
|
const currentProgress = currentAttachment.uploadProgress || 0
|
||||||
|
const increment =
|
||||||
|
currentProgress < 50
|
||||||
|
? Math.random() * 20 + 5 // Fast initial progress
|
||||||
|
: Math.random() * 8 + 2 // Slower progress as it approaches completion
|
||||||
|
|
||||||
|
currentAttachment = {
|
||||||
|
...currentAttachment,
|
||||||
|
uploadProgress: Math.min(85, currentProgress + increment),
|
||||||
|
}
|
||||||
|
onProgressUpdate?.(currentAttachment)
|
||||||
|
}
|
||||||
|
}, 150)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Actual upload
|
||||||
|
const response = await followApi.upload.uploadChatAttachment({ file: blob })
|
||||||
|
const serverUrl = response.data.url
|
||||||
|
|
||||||
|
// Clear progress interval
|
||||||
|
clearInterval(progressInterval)
|
||||||
|
|
||||||
|
// Update to 100% and completed status
|
||||||
|
const completedAttachment: FileAttachment = {
|
||||||
|
...fileAttachment,
|
||||||
|
serverUrl,
|
||||||
|
uploadStatus: "completed",
|
||||||
|
uploadProgress: 100,
|
||||||
|
errorMessage: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show 100% briefly before final callback
|
||||||
|
onProgressUpdate?.(completedAttachment)
|
||||||
|
|
||||||
|
return completedAttachment
|
||||||
|
} catch (uploadError) {
|
||||||
|
clearInterval(progressInterval)
|
||||||
|
throw uploadError
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Return attachment with error status
|
||||||
|
const errorAttachment: FileAttachment = {
|
||||||
|
...fileAttachment,
|
||||||
|
uploadStatus: "error",
|
||||||
|
uploadProgress: undefined,
|
||||||
|
errorMessage: error instanceof Error ? error.message : "Upload failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorAttachment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processAndUploadFile(
|
||||||
|
file: File,
|
||||||
|
options: ProcessFileOptions = {},
|
||||||
|
onProgressUpdate?: (attachment: FileAttachment) => void,
|
||||||
|
): Promise<ProcessFileResult> {
|
||||||
|
// First process the file locally
|
||||||
|
const localResult = await processFile(file, options)
|
||||||
|
|
||||||
|
if (!localResult.success || !localResult.fileAttachment) {
|
||||||
|
return localResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then upload to server
|
||||||
|
const uploadedAttachment = await uploadFileAttachment(
|
||||||
|
localResult.fileAttachment,
|
||||||
|
onProgressUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: uploadedAttachment.uploadStatus === "completed",
|
||||||
|
fileAttachment: uploadedAttachment,
|
||||||
|
error:
|
||||||
|
uploadedAttachment.uploadStatus === "error" ? uploadedAttachment.errorMessage : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
const MAX_IMAGE_ALLOWED_SIZE = 3 * 1024 * 1024
|
||||||
|
const MAX_DOCUMENT_ALLOWED_SIZE = 1 * 1024 * 1024
|
||||||
|
export const SUPPORTED_FILE_TYPES = {
|
||||||
|
// Images
|
||||||
|
"image/png": { extension: "png", category: "image", maxSize: MAX_IMAGE_ALLOWED_SIZE },
|
||||||
|
"image/jpeg": { extension: "jpg", category: "image", maxSize: MAX_IMAGE_ALLOWED_SIZE },
|
||||||
|
"image/jpg": { extension: "jpg", category: "image", maxSize: MAX_IMAGE_ALLOWED_SIZE },
|
||||||
|
"image/webp": { extension: "webp", category: "image", maxSize: MAX_IMAGE_ALLOWED_SIZE },
|
||||||
|
"image/gif": { extension: "gif", category: "image", maxSize: MAX_IMAGE_ALLOWED_SIZE },
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
"application/pdf": { extension: "pdf", category: "document", maxSize: MAX_DOCUMENT_ALLOWED_SIZE },
|
||||||
|
"text/plain": { extension: "txt", category: "text", maxSize: MAX_DOCUMENT_ALLOWED_SIZE },
|
||||||
|
"text/markdown": { extension: "md", category: "text", maxSize: MAX_DOCUMENT_ALLOWED_SIZE },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SupportedFileType = keyof typeof SUPPORTED_FILE_TYPES
|
||||||
|
export type FileCategory = (typeof SUPPORTED_FILE_TYPES)[SupportedFileType]["category"]
|
||||||
|
|
||||||
|
export interface FileValidationError {
|
||||||
|
type: "unsupported" | "too_large" | "invalid"
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileValidationResult {
|
||||||
|
isValid: boolean
|
||||||
|
error?: FileValidationError
|
||||||
|
fileInfo?: {
|
||||||
|
type: SupportedFileType
|
||||||
|
category: FileCategory
|
||||||
|
extension: string
|
||||||
|
maxSize: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateFile(file: File): FileValidationResult {
|
||||||
|
const fileType = file.type as SupportedFileType
|
||||||
|
const fileInfo = SUPPORTED_FILE_TYPES[fileType]
|
||||||
|
|
||||||
|
if (!fileInfo) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: {
|
||||||
|
type: "unsupported",
|
||||||
|
message: `File type "${file.type}" is not supported. Supported types: images, PDFs, text files.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > fileInfo.maxSize) {
|
||||||
|
const maxSizeMB = Math.round(fileInfo.maxSize / (1024 * 1024))
|
||||||
|
const fileSizeMB = Math.round((file.size / (1024 * 1024)) * 100) / 100
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: {
|
||||||
|
type: "too_large",
|
||||||
|
message: `File size (${fileSizeMB}MB) exceeds the maximum allowed size of ${maxSizeMB}MB for ${fileInfo.category} files.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size === 0) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: {
|
||||||
|
type: "invalid",
|
||||||
|
message: "File appears to be empty or corrupted.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
fileInfo: {
|
||||||
|
type: fileType,
|
||||||
|
category: fileInfo.category,
|
||||||
|
extension: fileInfo.extension,
|
||||||
|
maxSize: fileInfo.maxSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return "0 Bytes"
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB"]
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileCategoryFromMimeType(mimeType: string): FileCategory {
|
||||||
|
// Images
|
||||||
|
if (mimeType.startsWith("image/")) {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
if (mimeType === "application/pdf") {
|
||||||
|
return "document"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text files
|
||||||
|
if (mimeType.startsWith("text/")) {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileIconName(category: FileCategory): string {
|
||||||
|
switch (category) {
|
||||||
|
case "image": {
|
||||||
|
return "i-mgc-pic-cute-re"
|
||||||
|
}
|
||||||
|
case "document": {
|
||||||
|
return "i-mgc-file-cute-re"
|
||||||
|
}
|
||||||
|
case "text": {
|
||||||
|
return "i-mgc-document-cute-re"
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return "i-mgc-attachment-cute-re"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,10 +13,11 @@ export const AddCuteFiIcon = ({
|
||||||
color = "#10161F",
|
color = "#10161F",
|
||||||
}: AddCuteFiIconProps) => {
|
}: AddCuteFiIconProps) => {
|
||||||
return (
|
return (
|
||||||
<Svg width={width} height={height} viewBox="0 0 24 24">
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
<Path
|
<Path
|
||||||
|
d="M11.493 3.592a1.54 1.54 0 0 0-.844.768l-.109.22-.02 2.96-.02 2.96-2.96.02-2.96.02-.221.109a1.677 1.677 0 0 0-.71.71c-.098.198-.109.265-.109.641 0 .377.011.443.11.644.132.268.475.606.74.728.189.087.208.088 3.15.108l2.96.02.02 2.96c.02 2.942.021 2.961.108 3.15.122.265.46.608.728.74.201.099.267.11.644.11s.443-.011.644-.11c.268-.132.606-.475.728-.74.087-.189.088-.208.108-3.15l.02-2.96 2.96-.02 2.96-.02.224-.11c.268-.132.606-.475.728-.74.127-.275.127-.945 0-1.22-.122-.265-.46-.608-.728-.74l-.224-.11-2.96-.02-2.96-.02-.02-2.96c-.02-2.942-.021-2.961-.108-3.15-.122-.265-.46-.608-.725-.738-.295-.145-.838-.173-1.154-.06"
|
||||||
fill={color}
|
fill={color}
|
||||||
d="M11.283.099a2.18 2.18 0 0 0-1.196 1.088l-.155.312-.028 4.194-.029 4.194-4.196.028-4.197.029-.313.154a2.378 2.376 0 0 0-1.007 1.006c-.14.28-.155.376-.155.908 0 .535.016.628.156.913.187.38.674.859 1.05 1.032.267.123.294.124 4.466.153l4.196.028.029 4.194c.028 4.169.03 4.196.153 4.464.173.375.652.861 1.032 1.048.285.14.379.156.913.156.535 0 .628-.016.913-.156.38-.187.86-.673 1.032-1.048.124-.268.125-.295.154-4.464l.028-4.194 4.197-.028 4.196-.029.318-.156c.38-.187.86-.673 1.032-1.048.18-.39.18-1.34 0-1.729-.173-.375-.652-.861-1.032-1.048l-.318-.156-4.196-.029-4.197-.028-.028-4.194c-.029-4.169-.03-4.196-.154-4.464-.173-.375-.652-.861-1.028-1.045-.418-.206-1.188-.246-1.636-.085"
|
fillRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface AiCuteFiIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AiCuteFiIcon = ({ width = 24, height = 24, color = "#10161F" }: AiCuteFiIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M18.664 2.061c-.366.139-.508.319-.708.899-.225.652-.344.771-.996.996-.476.164-.61.247-.761.465a.99.99 0 0 0 0 1.158c.151.218.285.301.761.465.652.225.771.344.996.996.164.476.247.61.465.761a.99.99 0 0 0 1.158 0c.223-.154.301-.285.483-.814.205-.594.331-.72.925-.925.529-.182.66-.26.814-.483a.99.99 0 0 0 0-1.158c-.151-.218-.285-.301-.761-.465-.662-.228-.761-.327-.998-1.001-.087-.246-.21-.512-.274-.591-.242-.299-.749-.438-1.104-.303m-7.957 2.063c-.574.063-1.17.472-1.436.985a10.14 10.14 0 0 0-.389.987c-.289.833-.467 1.234-.758 1.702a7.219 7.219 0 0 1-2.326 2.326c-.468.291-.863.467-1.736.773-.915.32-1.166.45-1.431.74-.716.782-.698 2.023.04 2.761.292.291.477.385 1.401.709.863.303 1.259.479 1.726.769 1.161.721 2.141 1.812 2.686 2.987.082.178.268.662.413 1.075.32.915.45 1.166.74 1.431.779.713 2.026.694 2.761-.04.292-.292.385-.478.709-1.405.315-.899.554-1.407.927-1.964a6.979 6.979 0 0 1 1.926-1.926c.557-.373 1.064-.612 1.964-.927.927-.324 1.113-.417 1.405-.709.752-.752.752-2.044 0-2.796-.292-.291-.477-.385-1.401-.709-.863-.303-1.259-.479-1.726-.769a7.176 7.176 0 0 1-2.305-2.291c-.313-.506-.483-.885-.794-1.771-.32-.915-.45-1.166-.74-1.43-.257-.236-.692-.459-.958-.492-.354-.043-.427-.045-.698-.016"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface ArrowUpCircleCuteFiIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArrowUpCircleCuteFiIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: ArrowUpCircleCuteFiIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M11.28 2.024c-2.109.185-3.979.926-5.561 2.201-1.675 1.351-2.908 3.28-3.416 5.346-.216.881-.277 1.41-.277 2.429s.061 1.548.277 2.429c.886 3.607 3.839 6.502 7.457 7.311.844.189 1.287.236 2.24.236.953 0 1.396-.047 2.24-.236 3.618-.809 6.571-3.704 7.457-7.311.213-.869.276-1.413.278-2.409.001-.976-.043-1.404-.235-2.26-.458-2.049-1.658-4.025-3.26-5.369-1.824-1.531-3.915-2.321-6.26-2.368a15.89 15.89 0 0 0-.94.001m1.38 4.978c.71.269 1.915 1.213 2.595 2.034.474.572 1.069 1.506 1.21 1.899.073.205.07.552-.007.736a1.125 1.125 0 0 1-.59.553 1.06 1.06 0 0 1-.845-.08c-.198-.116-.24-.174-.692-.944a5.574 5.574 0 0 0-.949-1.233l-.362-.372L13 13.07l-.02 3.476-.121.197a.998.998 0 0 1-1.718 0l-.121-.197L11 13.07l-.02-3.475-.364.372c-.449.46-.753.871-1.114 1.507-.175.308-.329.528-.417.594a.984.984 0 0 1-1.275-.041.984.984 0 0 1-.288-1.055c.128-.39.727-1.34 1.211-1.921.826-.99 2.098-1.936 2.863-2.131.243-.062.797-.019 1.064.082"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface AtCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AtCuteReIcon = ({ width = 24, height = 24, color = "#10161F" }: AtCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M11.2 2.04a9.993 9.993 0 0 0-9.038 8.252c-.643 3.731.879 7.523 3.918 9.754a10.003 10.003 0 0 0 7.88 1.75c1.03-.202 2.343-.688 2.697-.999.527-.463.416-1.285-.217-1.61-.317-.162-.613-.141-1.1.08-2.211 1.004-4.674.975-6.84-.081-1.595-.777-2.929-2.109-3.679-3.672-.818-1.706-1.031-3.587-.604-5.334.593-2.425 2.154-4.353 4.386-5.415 1.779-.847 3.872-.998 5.862-.423a7.728 7.728 0 0 1 4.954 4.356c.4.927.578 1.716.609 2.702.057 1.85-.48 3.194-1.477 3.694-.36.18-.75.282-.946.247-.334-.061-.576-.347-.627-.743-.014-.103.087-1.277.239-2.785.145-1.433.263-2.707.263-2.831a.855.855 0 0 0-.241-.637.994.994 0 0 0-.681-.335.96.96 0 0 0-.8.341l-.136.161-.241-.223c-.985-.913-2.492-1.401-3.861-1.25-2.392.263-4.231 2.085-4.48 4.439-.22 2.082.882 4.06 2.781 4.995.343.168.826.344 1.069.388.095.018.15.05.15.088 0 .053.107.058.83.041.912-.023 1.275-.077 1.843-.276a4.736 4.736 0 0 0 1.224-.641c.175-.126.333-.219.351-.208a.82.82 0 0 1 .118.188c.144.283.69.789 1.054.976.687.353 1.585.388 2.427.096 1.644-.571 2.676-1.956 3.042-4.085.118-.681.126-2.082.017-2.74-.358-2.157-1.291-3.978-2.798-5.458-1.536-1.51-3.492-2.453-5.688-2.742a13.715 13.715 0 0 0-2.26-.06m1.64 7.073c.368.112.542.192.845.388 1.032.666 1.563 1.905 1.321 3.076a2.993 2.993 0 0 1-1.626 2.105c-.484.238-.754.297-1.36.297-.432 0-.571-.016-.819-.094-1.536-.478-2.437-1.929-2.145-3.451.115-.594.351-1.04.798-1.505a3.04 3.04 0 0 1 1.547-.869c.355-.081 1.09-.053 1.439.053"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface AttachmentCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AttachmentCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: AttachmentCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
stroke={color}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="m12.877 4.308 6.54 6.54a5.25 5.25 0 0 1 0 7.425v0a5.25 5.25 0 0 1-7.424 0l-7.954-7.954a3.501 3.501 0 0 1 0-4.952v0a3.501 3.501 0 0 1 4.952 0l7.953 7.953a1.751 1.751 0 0 1 0 2.477v0a1.751 1.751 0 0 1-2.477 0L7.22 8.55"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface BookmarkCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookmarkCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: BookmarkCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M9.5 2.545c-2.157.124-3.278.692-3.917 1.986-.569 1.154-.597 1.609-.572 9.409.015 4.685.026 5.484.08 5.76.161.838.473 1.31 1.055 1.599.197.098.295.119.622.133.804.035 1.183-.161 3.196-1.656 1.68-1.248 1.855-1.345 2.229-1.234.243.072.608.316 1.843 1.234 1.56 1.158 1.968 1.416 2.504 1.581.988.305 1.908-.162 2.24-1.137.192-.566.19-.494.208-6.26.025-7.816-.002-8.266-.573-9.429-.552-1.126-1.547-1.732-3.175-1.934-.355-.044-5.123-.087-5.74-.052m5.231 2.017c1.09.087 1.651.333 1.908.836.108.212.23.671.286 1.082.033.242.052 2.023.064 6.16.016 5.471.004 6.72-.066 6.72-.057 0-.717-.462-1.683-1.179-1.294-.96-1.591-1.161-2.021-1.366-.45-.214-.781-.294-1.219-.294-.438 0-.769.08-1.219.294-.43.205-.727.406-2.021 1.366-.954.708-1.626 1.179-1.681 1.179-.015 0-.039-.148-.053-.33-.042-.546-.029-11.183.015-11.946.074-1.267.26-1.825.71-2.123.44-.292.987-.394 2.349-.441 1.121-.039 3.95-.013 4.631.042"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface Comment2CuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Comment2CuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: Comment2CuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M10.48 3.086c-2.843.139-5.566 1.753-7.088 4.202a9.697 9.697 0 0 0-1.166 2.892c-.162.776-.199 1.228-.173 2.158.03 1.123.175 1.893.536 2.859l.142.379-.045.722a24.953 24.953 0 0 0-.046 1.342c.001.992.173 1.531.648 2.025.294.306.657.504 1.137.62.336.08.41.083 1.395.053 1.755-.053 1.642-.061 2.26.159.946.336 1.971.504 2.283.373.199-.084.426-.292.528-.486.065-.123.087-.234.087-.444 0-.331-.088-.529-.33-.748-.178-.161-.307-.211-.708-.273a7.809 7.809 0 0 1-1.44-.381c-.834-.3-.701-.291-2.565-.177-.669.041-1.137.013-1.223-.073-.086-.086-.114-.554-.073-1.223.024-.377.054-.928.067-1.225l.024-.54-.244-.74c-.308-.933-.403-1.391-.441-2.12-.081-1.538.301-2.959 1.132-4.216C7.14 5.257 10.908 4.225 14.1 5.779a6.943 6.943 0 0 1 2.191 1.679c.408.461.738.963 1.038 1.577.268.548.404.728.647.856.228.12.659.121.884.001a.974.974 0 0 0 .54-.891c-.001-.227-.026-.317-.196-.689-.888-1.947-2.531-3.583-4.457-4.443-1.386-.618-2.701-.86-4.267-.783m5.422 7.958c-1.258.133-2.552.768-3.384 1.661-1.646 1.767-1.982 4.363-.833 6.439a5.48 5.48 0 0 0 2.967 2.531c.632.223 1.089.298 1.828.301.721.003 1.105-.053 1.695-.247.309-.102.365-.108 1.145-.108.74-.001.842-.009 1.047-.087a2.022 2.022 0 0 0 1.167-1.167c.078-.205.086-.307.087-1.047.001-.779.007-.836.108-1.145.199-.607.249-.942.249-1.655-.002-1.194-.279-2.122-.921-3.087-1.117-1.678-3.114-2.604-5.155-2.389m1.478 2.074c1.221.318 2.165 1.26 2.507 2.502.072.263.088.423.086.9-.001.558-.007.598-.152 1.06-.226.72-.229.743-.199 1.431l.028.63-.735-.01-.735-.011-.446.149c-.565.189-.934.246-1.414.22a3.509 3.509 0 0 1-3.309-3.309c-.094-1.711 1.082-3.231 2.789-3.605.437-.096 1.116-.078 1.58.043"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface CommentCuteFiIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentCuteFiIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: CommentCuteFiIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M6.619 4.041c-.768.062-1.235.185-1.83.484-.516.26-.898.536-1.313.951-.754.754-1.22 1.647-1.386 2.654-.058.355-.067.768-.069 3.07l-.001 2.66.094.36c.2.766.667 1.495 1.261 1.965.382.302.935.585 1.331.68l.282.068.024.623c.035.931.203 1.367.644 1.671.631.435 1.262.321 2.246-.407l.297-.22-.183-.268c-.794-1.16-1.15-2.726-1.02-4.485.079-1.065.256-1.76.647-2.541.823-1.643 2.299-2.771 4.183-3.195.385-.087.465-.089 3.434-.102 1.672-.007 3.171.003 3.33.022.279.033.29.031.29-.049 0-.174-.172-.697-.357-1.086a4.762 4.762 0 0 0-.999-1.42c-.74-.739-1.635-1.213-2.615-1.383-.317-.055-.896-.065-4.129-.074-2.068-.006-3.941.004-4.161.022m5.722 6.021c-1.46.245-2.653 1.269-3.138 2.694-.285.839-.285 2.653.001 3.488.356 1.041 1.052 1.844 2.016 2.325.7.35 1.165.431 2.469.431h.948l.892.742c.49.409.996.813 1.125.9.545.366 1.158.374 1.691.02.409-.271.599-.657.641-1.303l.026-.399.164-.042a3.66 3.66 0 0 0 1.224-.521c.691-.447 1.224-1.181 1.469-2.027.096-.328.103-.419.121-1.457.02-1.199-.021-1.653-.195-2.161-.492-1.439-1.683-2.451-3.164-2.692-.511-.082-5.795-.081-6.29.002"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface CommentCuteLiIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentCuteLiIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: CommentCuteLiIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M6.544 4.281c-1.978.182-3.645 1.616-4.146 3.567-.139.541-.165 1.201-.148 3.652.016 2.213.021 2.335.103 2.649.332 1.277 1.309 2.23 2.572 2.509l.306.068.017.787c.019.894.05 1.015.349 1.339.242.262.503.366.923.367.307.001.361-.011.56-.123.316-.178.453-.388.453-.689a.731.731 0 0 0-.618-.744l-.146-.025-.017-.789c-.015-.726-.024-.803-.109-.969-.214-.419-.472-.57-1.083-.636-.337-.036-.478-.073-.712-.184a1.944 1.944 0 0 1-.919-.928l-.149-.312-.011-2.599c-.012-2.812-.007-2.897.207-3.426.168-.416.354-.685.744-1.075.391-.391.659-.576 1.08-.746.528-.213.54-.214 4.702-.214 2.581 0 3.924.014 4.066.043.939.19 1.632.66 2.188 1.484.262.388.439.511.736.512a.729.729 0 0 0 .748-.731c0-.32-.345-.87-.89-1.421a4.874 4.874 0 0 0-2.37-1.298c-.349-.081-.475-.084-4.22-.092-2.123-.004-4.02.007-4.216.024m5.051 5.002c-.425.045-.76.138-1.143.317-.855.399-1.406.94-1.813 1.78-.335.689-.398 1.103-.398 2.62 0 1.462.064 1.918.359 2.548.399.855.94 1.406 1.78 1.813.722.351 1.073.397 3.002.398l1.362.001 1.038.866c.571.476 1.119.904 1.218.95.28.13.75.118 1.047-.028.268-.131.447-.322.585-.624.085-.184.104-.296.119-.702l.019-.483.325-.063c.618-.122 1.094-.368 1.573-.812.503-.468.803-.96.977-1.604.078-.291.088-.446.105-1.72.026-1.985-.026-2.414-.389-3.16-.407-.84-.958-1.381-1.813-1.78-.399-.187-.7-.267-1.193-.319-.447-.047-6.314-.045-6.76.002m6.979 1.54c.376.097.697.287 1.006.597.311.311.5.63.598 1.012.056.217.064.48.054 1.863l-.012 1.609-.148.301a1.967 1.967 0 0 1-.888.879c-.2.094-.353.13-.671.161-.475.046-.674.125-.908.36-.239.239-.319.438-.354.884l-.031.386-.811-.673c-.874-.726-1.096-.86-1.521-.922-.147-.021-.931-.039-1.742-.039-1.629-.001-1.753-.015-2.206-.254-.309-.163-.764-.618-.927-.927-.235-.447-.253-.591-.253-2.06s.018-1.613.253-2.06c.157-.298.614-.761.907-.918.493-.265.351-.256 4.054-.259 2.937-.003 3.385.005 3.6.06"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface CommentCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: CommentCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M6.4 4.043c-2.015.233-3.73 1.736-4.239 3.717-.147.574-.168 1.066-.151 3.66.017 2.664.014 2.624.234 3.22.404 1.092 1.516 2.058 2.602 2.26l.146.027.015.727c.014.695.02.736.125.951.163.335.392.572.699.724.226.111.319.133.608.144.394.016.687-.071.97-.287.367-.28.485-.829.267-1.246-.114-.217-.382-.451-.558-.486l-.11-.022-.015-.706c-.013-.663-.02-.72-.119-.932a1.344 1.344 0 0 0-1.117-.777c-.607-.053-.931-.186-1.26-.514a1.62 1.62 0 0 1-.455-.823c-.035-.162-.044-.889-.034-2.74.014-2.514.014-2.521.107-2.8A3.141 3.141 0 0 1 6.14 6.114l.28-.094 3.9-.012c2.715-.008 3.979.002 4.16.033.858.145 1.56.624 2.11 1.439.309.457.496.567.933.551.256-.009.334-.03.499-.132.288-.178.433-.434.451-.795.013-.261.003-.306-.125-.551-.2-.384-.54-.819-.925-1.183a5.1 5.1 0 0 0-2.363-1.261c-.353-.083-.442-.085-4.36-.092-2.2-.005-4.135.007-4.3.026m4.958 5.018a4.013 4.013 0 0 0-2.894 2.099C8.064 11.942 8 12.33 8 14s.064 2.058.464 2.84a3.978 3.978 0 0 0 1.696 1.696c.804.411 1.147.463 3.051.464h1.43l1.01.842c.575.48 1.104.888 1.229.95.195.095.266.108.62.108.361 0 .423-.011.638-.117.315-.154.56-.407.713-.735.108-.229.129-.328.147-.684l.022-.416.28-.068c1.048-.251 1.974-1.038 2.402-2.04.257-.604.27-.711.287-2.44.021-2.073-.025-2.404-.453-3.24-.46-.899-1.307-1.622-2.291-1.957-.566-.192-.795-.203-4.285-.199-2.502.003-3.339.016-3.602.057m7.134 1.999c.36.093.626.25.912.536.296.295.448.561.536.933.081.346.086 2.99.006 3.291a1.423 1.423 0 0 1-.433.726c-.334.323-.459.37-1.307.493a1.376 1.376 0 0 0-1.165 1.093l-.048.232-.206-.174c-.954-.803-1.101-.91-1.447-1.053l-.28-.115-1.76-.023c-1.701-.021-1.767-.025-1.984-.111a2.087 2.087 0 0 1-1.256-1.396c-.087-.331-.087-2.653 0-2.984.093-.36.25-.626.536-.912.28-.28.55-.442.895-.534.32-.087 6.67-.089 7.001-.002"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface DocumentsCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentsCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: DocumentsCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M10.423 1.581c-1.02.142-1.841.583-2.47 1.327-.581.687-.836 1.391-.911 2.517-.019.298-.044.551-.055.561-.01.011-.201.037-.423.057a3.876 3.876 0 0 0-2.383 1.138 3.87 3.87 0 0 0-1.14 2.408c-.053.557-.053 6.885 0 8.109.063 1.464.171 2.01.549 2.782.201.412.263.498.587.82.659.654 1.352.944 2.647 1.109.289.036 1.275.05 3.636.05l3.24.001.32-.09c1.295-.366 2.378-1.386 2.773-2.61.094-.295.15-.602.187-1.044l.026-.305.267-.023c.375-.032.85-.134 1.236-.265.526-.18.905-.417 1.314-.823.324-.322.386-.408.587-.82.38-.776.488-1.325.549-2.805.05-1.184.052-5.033.003-5.615a5.586 5.586 0 0 0-.546-2.058c-.337-.713-.636-1.104-1.572-2.054-1.138-1.154-1.485-1.433-2.265-1.821a6.077 6.077 0 0 0-1.399-.496c-.337-.079-.49-.085-2.36-.095-1.532-.008-2.093.003-2.397.045m2.145 2.004c.683.201 1.26.836 1.391 1.53.023.119.041.41.041.647 0 .555.081.968.263 1.347.266.554.928 1.117 1.517 1.29.088.026.475.063.86.081.757.037 1.001.089 1.347.285.475.269.878.837.978 1.375.055.299.019 3.877-.044 4.38-.12.953-.361 1.393-.901 1.646-.578.27-1.259.326-4.02.326-2.761 0-3.442-.056-4.02-.326-.555-.26-.786-.697-.909-1.721-.073-.607-.074-9.035-.001-9.385.073-.348.268-.716.507-.955.419-.419.756-.541 1.622-.586.693-.037 1.081-.018 1.369.066m3.496.54c.603.394 2 1.798 2.327 2.34.187.31.202.357.099.315-.368-.152-.788-.227-1.43-.254-.988-.043-1.06-.102-1.06-.875 0-.494-.086-1.062-.221-1.461-.05-.148-.077-.27-.06-.27s.172.092.345.205M7.001 10.39c.001 4.286.074 5.038.589 6.09.201.412.263.498.587.82.647.642 1.35.942 2.587 1.102.185.023 1.2.053 2.256.065 1.056.011 1.932.033 1.948.047.015.014.012.15-.007.302-.069.549-.492 1.163-.988 1.433-.465.253-.442.251-3.413.247-3.367-.005-3.979-.049-4.58-.33-.426-.2-.625-.457-.778-1.007-.172-.622-.182-.919-.182-5.339 0-3.991.004-4.211.075-4.42.278-.822.949-1.332 1.835-1.395.066-.005.07.134.071 2.385"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface HistoryCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HistoryCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: HistoryCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M11.32 2.027c-.498.036-1.238.156-1.74.281-3.929.981-6.849 4.201-7.466 8.232-.114.748-.124 2.091-.021 2.8.553 3.779 3.003 6.786 6.587 8.084.508.184 1.211.36 1.853.464.717.115 2.101.124 2.807.017 2.014-.305 3.683-1.056 5.18-2.329.388-.331.507-.513.546-.841a.927.927 0 0 0-.205-.705c-.195-.256-.443-.37-.801-.369-.332.001-.431.047-.81.373-1.591 1.366-3.458 2.023-5.55 1.954-.669-.022-.884-.048-1.44-.173a8.081 8.081 0 0 1-4.007-2.267c-1.155-1.211-1.832-2.553-2.142-4.248-.116-.635-.124-1.951-.014-2.54.32-1.73.983-3.063 2.126-4.275a7.977 7.977 0 0 1 7.605-2.265c2.916.697 5.181 2.919 5.913 5.8.072.286.144.608.159.716.024.173.019.192-.042.169a10.93 10.93 0 0 0-.553-.15c-.377-.095-.543-.119-.745-.105a1.09 1.09 0 0 0-.958.638c-.104.21-.122.289-.122.554 0 .261.017.341.11.513.061.112.497.649.97 1.191 1.064 1.222 1.24 1.349 1.86 1.349.354.001.643-.096.901-.3.508-.401.651-.967.654-2.595.003-.949-.016-1.16-.181-2-.347-1.77-1.275-3.552-2.549-4.892-1.255-1.32-3.01-2.348-4.749-2.783-1.046-.261-2.198-.369-3.176-.298m.353 4.036c-.261.08-.533.358-.612.627-.087.293-.089 4.101-.003 4.67.108.702.395 1.351.848 1.913.135.169.766.823 1.4 1.453 1.084 1.076 1.167 1.149 1.371 1.21.27.08.374.08.633.003.279-.083.546-.35.629-.629.077-.259.077-.363-.003-.632-.061-.204-.14-.293-1.332-1.498-.882-.892-1.299-1.342-1.374-1.483-.219-.413-.229-.535-.229-2.76-.001-2.341 0-2.329-.304-2.634-.279-.279-.63-.361-1.024-.24"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface MicCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MicCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: MicCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M11.4 2.044c-2.009.227-3.733 1.739-4.238 3.716C7.019 6.317 7 6.766 7 9.5s.019 3.183.162 3.74c.393 1.541 1.541 2.832 3.045 3.423 1.091.43 2.495.43 3.586 0a5.08 5.08 0 0 0 3.062-3.483c.124-.495.145-1.022.145-3.68 0-2.732-.019-3.181-.161-3.74-.441-1.736-1.859-3.154-3.599-3.598a5.223 5.223 0 0 0-1.84-.118m1.46 2.077c.965.3 1.721 1.06 2.026 2.039l.094.3v6.08l-.094.3a3.086 3.086 0 0 1-2.046 2.046c-.434.135-1.246.135-1.68 0a3.086 3.086 0 0 1-2.046-2.046l-.094-.3V6.46l.094-.3c.342-1.095 1.259-1.921 2.357-2.121.326-.06 1.075-.016 1.389.082m-8.115 7.938c-.367.131-.665.534-.665.898 0 .218.114.82.238 1.258.711 2.514 2.718 4.584 5.222 5.386a8.45 8.45 0 0 0 1.25.299l.21.028v.609c0 .731.045.902.306 1.163.18.179.458.3.694.3.237 0 .514-.12.697-.303.254-.255.303-.445.303-1.181v-.588l.21-.028c.115-.015.39-.07.61-.121a8.008 8.008 0 0 0 5.615-4.847c.249-.626.485-1.579.485-1.958 0-.622-.679-1.124-1.264-.933-.438.142-.64.431-.777 1.111-.382 1.888-1.58 3.434-3.295 4.251a6.973 6.973 0 0 1-1.204.435c-.57.157-1.517.201-2.2.103A6.036 6.036 0 0 1 6.1 13.08c-.085-.457-.212-.713-.434-.87-.301-.213-.607-.263-.921-.151"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface MindMapCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MindMapCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: MindMapCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M13.499 2.04a3.042 3.042 0 0 0-2.374 2.099c-.138.456-.144 1.218-.012 1.681.15.526.426.971.853 1.373l.186.176-.11.325-.109.325-.281-.013c-.837-.038-1.858.24-2.554.698l-.284.186c-.028.018-.21-.106-.46-.316-.342-.286-.413-.364-.408-.45.003-.057.018-.266.034-.464.127-1.634-1.379-2.939-3-2.6a2.502 2.502 0 0 0-1.695 1.302c-.375.762-.375 1.514 0 2.276.538 1.091 1.834 1.614 3.025 1.221l.326-.108.424.351c.313.26.416.368.396.415a15.61 15.61 0 0 0-.159.403c-.512 1.333-.326 2.925.478 4.092l.188.273-.392.468-.391.468-.32-.097c-.27-.081-.404-.096-.86-.097-.459 0-.588.015-.861.098a3.06 3.06 0 0 0-2.014 2.014c-.083.273-.098.403-.098.861 0 .458.015.588.098.861a3.06 3.06 0 0 0 2.014 2.014c.273.083.403.098.861.098.458 0 .588-.015.861-.098a3.06 3.06 0 0 0 2.014-2.014c.137-.454.144-1.219.013-1.674a3.394 3.394 0 0 0-.164-.46l-.075-.147.408-.489c.224-.269.418-.5.432-.513.013-.013.192.039.397.116.591.22.93.279 1.614.279.638 0 1.007-.057 1.483-.23l.251-.091.466.468.467.468-.066.216c-.1.332-.12.844-.046 1.196a2.504 2.504 0 0 0 2.965 1.94 2.527 2.527 0 0 0 1.92-1.92A2.504 2.504 0 0 0 17 16.055a2.767 2.767 0 0 0-1.196.046l-.216.066-.35-.349-.35-.348.238-.322a4.74 4.74 0 0 0 .68-1.358l.09-.31h1.299l.132.25c.076.142.255.373.415.534a2.486 2.486 0 0 0 3.52-.002 2.488 2.488 0 0 0 0-3.524 2.486 2.486 0 0 0-3.52-.002c-.16.161-.339.392-.415.534l-.132.25h-1.299l-.09-.31c-.191-.657-.627-1.384-1.13-1.886-.161-.161-.742-.629-.825-.665a3.12 3.12 0 0 1 .097-.327l.106-.324.263-.028a3.024 3.024 0 0 0 2.558-2.119c.083-.273.098-.403.098-.861 0-.458-.015-.588-.098-.861-.291-.956-1.071-1.734-2.014-2.01-.361-.106-1.029-.149-1.362-.089m1.013 2.107c.63.387.642 1.299.023 1.692a.998.998 0 0 1-1.394-.336c-.107-.174-.121-.232-.121-.5 0-.259.016-.33.111-.492a.955.955 0 0 1 .941-.496.836.836 0 0 1 .44.132M5.724 7.062a.488.488 0 0 1 .198.686.485.485 0 0 1-.844 0 .478.478 0 0 1 .174-.67.453.453 0 0 1 .472-.016m6.596 3.074c.71.248 1.284.822 1.547 1.544.098.269.109.354.109.82 0 .466-.011.551-.109.82a2.589 2.589 0 0 1-1.547 1.548c-.268.097-.355.108-.82.108-.466 0-.551-.011-.82-.109a2.567 2.567 0 0 1-1.544-1.547 2.755 2.755 0 0 1-.082-1.35c.201-.943.994-1.727 1.949-1.929.328-.069.983-.022 1.317.095m7.404 1.926A.56.56 0 0 1 20 12.5c0 .248-.252.5-.5.5-.244 0-.5-.252-.5-.492 0-.251.248-.508.492-.508a.61.61 0 0 1 .232.062M6.512 18.147c.63.387.642 1.299.023 1.692a.998.998 0 0 1-1.394-.336c-.107-.174-.121-.232-.121-.5 0-.259.016-.33.111-.492a.955.955 0 0 1 .941-.496.836.836 0 0 1 .44.132m10.212-.085A.56.56 0 0 1 17 18.5c0 .248-.252.5-.5.5-.244 0-.5-.252-.5-.492 0-.251.248-.508.492-.508a.61.61 0 0 1 .232.062"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface PicCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PicCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: PicCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M7.24 2.546c-2.11.108-3.311.502-4.229 1.387C2.33 4.59 1.93 5.422 1.72 6.619c-.17.975-.18 1.264-.18 5.421 0 3.543.008 4.047.07 4.588.197 1.694.588 2.656 1.401 3.439.804.775 1.708 1.131 3.361 1.323.549.063 1.092.07 5.628.07s5.079-.007 5.628-.07c1.653-.192 2.557-.548 3.361-1.323.806-.777 1.208-1.76 1.4-3.427.064-.555.071-1.04.071-4.64 0-3.581-.008-4.086-.07-4.628-.197-1.694-.588-2.656-1.401-3.439-.796-.768-1.698-1.126-3.32-1.319-.502-.06-1.134-.069-5.369-.075-2.64-.003-4.917 0-5.06.007M17 4.557c1.444.126 2.132.344 2.611.826.503.507.709 1.168.836 2.677.056.667.076 7.39.023 7.443-.017.017-.359-.293-.76-.689-.834-.821-1.056-1.005-1.443-1.2-.574-.29-1.37-.383-2.007-.236-.366.084-.857.33-1.173.585l-.252.205-1.308-1.295c-.719-.713-1.46-1.417-1.648-1.565-.765-.605-1.301-.827-1.999-.827-.437 0-.775.081-1.177.283-.649.324-.884.534-3.212 2.851-1.061 1.056-1.943 1.907-1.96 1.89-.054-.055-.034-6.77.022-7.445.127-1.507.337-2.181.837-2.682.567-.566 1.487-.792 3.49-.858 1.387-.046 8.503-.017 9.12.037m-1.85 2.484a1.618 1.618 0 0 0-1.017.859c-.096.207-.112.29-.111.6 0 .309.016.395.112.604.141.309.45.617.766.763.207.096.29.112.6.111.309 0 .395-.016.604-.112.309-.141.617-.45.763-.766.096-.207.112-.29.111-.6 0-.309-.016-.395-.112-.604a1.67 1.67 0 0 0-.756-.757c-.258-.117-.703-.163-.96-.098m-4.978 5.505c.311.16.737.556 2.788 2.596 1.199 1.193 2.241 2.2 2.316 2.238.211.106.568.125.788.041.396-.152.656-.524.655-.941-.001-.266-.095-.489-.304-.721l-.153-.169.134-.112c.312-.261.802-.243 1.154.041.105.084.71.675 1.345 1.312l1.155 1.16-.114.209c-.174.321-.496.617-.871.801-.887.436-1.991.517-7.065.517s-6.178-.081-7.065-.517c-.375-.184-.697-.48-.871-.801l-.114-.21 2.375-2.377c2.368-2.37 2.862-2.839 3.198-3.031a.696.696 0 0 1 .649-.036"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface SaveCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: SaveCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M9.3 1.545c-1.509.094-2.391.262-3.117.592-1.318.601-2.129 1.712-2.443 3.351-.185.964-.191 1.132-.209 5.992-.021 5.672.009 6.452.29 7.512.219.829.572 1.451 1.154 2.033.939.939 1.964 1.304 3.965 1.413.975.053 5.128.054 6.12.001 1.282-.068 2.108-.242 2.84-.6a3.935 3.935 0 0 0 1.208-.892c.666-.701 1.007-1.441 1.207-2.615.147-.869.171-1.966.157-7.272l-.013-5-.088-.311c-.259-.913-.529-1.309-1.67-2.45-1.142-1.141-1.54-1.413-2.45-1.67-.308-.086-.346-.087-3.531-.092-1.771-.002-3.31.001-3.42.008m6.477 2.031c.45.126.602.242 1.504 1.143.91.91 1.018 1.055 1.148 1.525.066.239.071.595.069 5.236-.002 5.241-.02 5.903-.182 6.7-.089.435-.235.84-.382 1.06a2.716 2.716 0 0 1-.9.798c-.637.319-1.158.388-3.344.446-1.792.047-4.726-.001-5.41-.088-.769-.099-1.351-.306-1.705-.607-.459-.39-.678-.748-.835-1.369-.145-.57-.179-.975-.225-2.68-.051-1.876-.02-7.931.044-8.74.135-1.693.465-2.474 1.236-2.93.218-.129.672-.285 1.065-.366.817-.169 2.149-.22 5.4-.205 1.98.01 2.311.02 2.517.077m-.499 5.542c-.164.05-.275.118-.668.408a22.814 22.814 0 0 0-3.576 3.334l-.441.52-.282-.274c-.527-.512-1.249-1.04-1.566-1.144a1.12 1.12 0 0 0-.711.054c-.38.181-.573.526-.547.98.021.377.136.535.653.901.51.36 1.13.98 1.449 1.449.301.441.427.553.72.638a.95.95 0 0 0 .995-.27c.068-.074.264-.32.434-.547a20.378 20.378 0 0 1 4.022-4.005c.48-.359.558-.436.665-.662a.985.985 0 0 0-.602-1.361c-.213-.064-.383-.07-.545-.021"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface SearchCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: SearchCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M9.84 2.025c-2.094.192-3.893 1.025-5.341 2.474-1.306 1.306-2.108 2.887-2.406 4.746-.092.572-.092 1.938 0 2.51.298 1.859 1.1 3.44 2.406 4.746 1.301 1.302 2.893 2.109 4.746 2.406.572.092 1.938.092 2.51 0a8.79 8.79 0 0 0 3.777-1.556l.231-.165 1.909 1.901c1.714 1.709 1.928 1.909 2.105 1.967.812.27 1.546-.45 1.283-1.259-.066-.202-.189-.334-1.972-2.123l-1.902-1.909.165-.231a8.79 8.79 0 0 0 1.556-3.777c.092-.572.092-1.938 0-2.51-.297-1.853-1.104-3.445-2.406-4.746a8.417 8.417 0 0 0-4.726-2.4c-.391-.06-1.587-.106-1.935-.074m1.761 2.07c2.735.497 4.797 2.564 5.309 5.325.092.495.092 1.665 0 2.16-.514 2.771-2.559 4.816-5.33 5.33-.495.092-1.665.092-2.16 0-2.504-.465-4.468-2.22-5.159-4.609a6.485 6.485 0 0 1 1.747-6.482 6.497 6.497 0 0 1 3.626-1.761c.478-.068 1.49-.049 1.967.037"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface SendPlaneCuteFiIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SendPlaneCuteFiIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: SendPlaneCuteFiIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M18.24 3.106c-1.06.135-1.761.296-5.82 1.337-4 1.027-5.141 1.334-6.032 1.623-2.111.686-3.077 1.403-3.494 2.594-.102.291-.112.371-.113.88-.001.476.013.605.092.86.319 1.026.988 1.905 2.881 3.783l1.134 1.126-.054.235c-.039.17-.054.574-.054 1.436 0 1.137.005 1.215.091 1.49a1.45 1.45 0 0 0 1.146 1.007c.562.097.985-.087 1.634-.714l.432-.416.095.076c.052.042.288.248.523.457 1.435 1.272 2.392 1.76 3.45 1.76 1.076 0 1.958-.528 2.669-1.598.315-.473.72-1.283 1.114-2.224.278-.664 2.284-5.814 2.834-7.278.896-2.381 1.136-3.534.928-4.472a2.545 2.545 0 0 0-1.836-1.891c-.237-.064-1.309-.111-1.62-.071m-.27 3.46a.39.39 0 0 1 .07.214c0 .12-.316.445-3.995 4.096a591.557 591.557 0 0 1-4.097 4.045c-.25.196-.63.248-.969.132-.229-.077-.841-.64-1.008-.925-.228-.388-.147-.944.181-1.257.136-.129 9.351-6.348 9.488-6.402.096-.039.254.008.33.097"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface SendPlaneCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SendPlaneCuteReIcon = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
color = "#10161F",
|
||||||
|
}: SendPlaneCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M18.36 3.083c-.939.087-1.744.271-6.12 1.396-4.7 1.209-5.393 1.399-6.36 1.749C3.764 6.994 2.8 8.03 2.8 9.54c0 .505.096.905.334 1.4.434.901 1.058 1.653 2.765 3.333l1.022 1.005-.045.171c-.025.097-.053.676-.064 1.331-.015.948-.008 1.213.043 1.452.116.552.365.884.825 1.103.227.108.303.124.602.124.317.001.365-.01.654-.153.245-.121.407-.245.747-.574l.434-.42.408.366c1.592 1.43 2.524 1.925 3.635 1.933 1.373.009 2.331-.749 3.235-2.559.415-.831.722-1.593 2.637-6.552 1.538-3.984 1.744-4.664 1.745-5.76 0-.865-.179-1.334-.719-1.887-.261-.267-.384-.359-.66-.491-.538-.258-1.233-.353-2.038-.279m-.241 2.092c-.06.055-9.84 8.139-10.019 8.282l-.097.077-.612-.596C5.292 10.892 4.646 9.986 4.81 9.32c.062-.255.429-.611.855-.831.889-.459 1.604-.669 6.955-2.045 3.923-1.008 5.186-1.313 5.479-1.322.068-.002.07.005.02.053m1.439 1.812c-.183.63-.515 1.529-1.591 4.313-1.786 4.62-2.017 5.194-2.386 5.92-.479.942-.936 1.416-1.367 1.419-.494.004-1.245-.455-2.326-1.42-.528-.471-2.408-2.262-2.407-2.293.001-.014.41-.364.91-.776l5.089-4.205c2.299-1.899 4.185-3.44 4.192-3.423.007.017-.044.226-.114.465"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Svg, { Path } from "react-native-svg"
|
||||||
|
|
||||||
|
interface UpCuteReIconProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpCuteReIcon = ({ width = 24, height = 24, color = "#10161F" }: UpCuteReIconProps) => {
|
||||||
|
return (
|
||||||
|
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
|
||||||
|
<Path
|
||||||
|
d="M11.476 8.258c-.271.075-.597.261-1.156.662a17.374 17.374 0 0 0-4.461 4.642c-.438.665-.497.795-.498 1.1-.003.748.766 1.211 1.442.868.186-.094.251-.161.455-.47.132-.198.382-.567.558-.82 1.015-1.466 2.518-2.949 3.954-3.899l.23-.152.23.152c1.436.95 2.939 2.433 3.954 3.899.176.253.426.622.558.82.204.309.269.376.455.47.676.343 1.445-.12 1.442-.868-.001-.305-.06-.435-.498-1.1A17.374 17.374 0 0 0 13.68 8.92c-.575-.412-.884-.587-1.175-.664-.252-.068-.781-.066-1.029.002"
|
||||||
|
fill={color}
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path stroke="#10161F" stroke-linecap="round" stroke-width="2" d="m12.877 4.308 6.54 6.54a5.25 5.25 0 0 1 0 7.425v0a5.25 5.25 0 0 1-7.424 0l-7.954-7.954a3.501 3.501 0 0 1 0-4.952v0a3.501 3.501 0 0 1 4.952 0l7.953 7.953a1.751 1.751 0 0 1 0 2.477v0a1.751 1.751 0 0 1-2.477 0L7.22 8.55"/></svg>
|
||||||
|
After Width: | Height: | Size: 422 B |
|
|
@ -7,8 +7,8 @@ settings:
|
||||||
catalogs:
|
catalogs:
|
||||||
default:
|
default:
|
||||||
'@follow-app/client-sdk':
|
'@follow-app/client-sdk':
|
||||||
specifier: 0.3.27
|
specifier: 0.3.29
|
||||||
version: 0.3.27
|
version: 0.3.29
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.8.3
|
specifier: 5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
|
|
@ -462,7 +462,7 @@ importers:
|
||||||
version: 3.0.2(electron@37.2.0)
|
version: 3.0.2(electron@37.2.0)
|
||||||
'@follow-app/client-sdk':
|
'@follow-app/client-sdk':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.27(@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.29(@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':
|
'@follow/database':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../packages/internal/database
|
version: link:../../../../packages/internal/database
|
||||||
|
|
@ -762,8 +762,8 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../packages/internal/utils
|
version: link:../../../../packages/internal/utils
|
||||||
'@folo-services/ai-tools':
|
'@folo-services/ai-tools':
|
||||||
specifier: 0.2.19
|
specifier: 0.2.20
|
||||||
version: 0.2.19(@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.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)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 24.0.10
|
specifier: 24.0.10
|
||||||
version: 24.0.10
|
version: 24.0.10
|
||||||
|
|
@ -1670,7 +1670,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@follow-app/client-sdk':
|
'@follow-app/client-sdk':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.27(@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.29(@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':
|
'@follow/configs':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../configs
|
version: link:../../configs
|
||||||
|
|
@ -1682,7 +1682,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@follow-app/client-sdk':
|
'@follow-app/client-sdk':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.27(@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.29(@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':
|
'@follow/constants':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../constants
|
version: link:../constants
|
||||||
|
|
@ -1760,7 +1760,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@follow-app/client-sdk':
|
'@follow-app/client-sdk':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.27(@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.29(@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':
|
'@follow/constants':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../constants
|
version: link:../constants
|
||||||
|
|
@ -1824,7 +1824,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@follow-app/client-sdk':
|
'@follow-app/client-sdk':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.27(@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.29(@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':
|
'@follow/configs':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../configs
|
version: link:../../configs
|
||||||
|
|
@ -4048,23 +4048,23 @@ packages:
|
||||||
'@floating-ui/utils@0.2.10':
|
'@floating-ui/utils@0.2.10':
|
||||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||||
|
|
||||||
'@follow-app/client-sdk@0.3.27':
|
'@follow-app/client-sdk@0.3.29':
|
||||||
resolution: {integrity: sha512-Zr4XtFpZ+Gx+CFxcqJwNTSqv0UaKfb5WKMxbHrJ/a9dzdL1MvkAnvO50sx5ViOCLur+bimgz9Epx8+4qGq5VTg==}
|
resolution: {integrity: sha512-bQgY0S6xrWO0iLO4eeptU4vxcmQ36iSrLl6omNscK6SCXzlebCYRmqQy7dsFHU4+bditADjjLulCGWmbDX9IJQ==}
|
||||||
|
|
||||||
'@folo-services/ai-tools@0.2.19':
|
'@folo-services/ai-tools@0.2.20':
|
||||||
resolution: {integrity: sha512-izmFV5zz7HvlyvMGQQS2xNZS/H6QjH+Uvw0n7AywTwkBsvAR1qHsuOGfNTEhb0mRr3EZumk0g9NlA73nmG6v2g==}
|
resolution: {integrity: sha512-1AJI48H2g64JhuTruSqQKUrn4Tc353hMFdKAbohh65AxTAMN/1pIw9w17Dtjm+g5BVlZUXYKaiIvaN9tHsXLNA==}
|
||||||
|
|
||||||
'@folo-services/constants@0.1.17':
|
'@folo-services/constants@0.1.18':
|
||||||
resolution: {integrity: sha512-IR+X76A/ScNj9jvgKdHv9Q1ozGlRo6ayEOpEAG3mwy1adlJdyG2eDlwHsUSMj9kiQUTwDF2JryG0GF32KkmUMw==}
|
resolution: {integrity: sha512-3LDxX4Ebvr6GLdL5Ce2ZkAJwfNjfsdqOLpuFRZ3bq2gVwQZbIGsln+qYnInmR5+1i3DtebglrAj/IktnT3BsIg==}
|
||||||
|
|
||||||
'@folo-services/drizzle@0.1.12':
|
'@folo-services/drizzle@0.1.13':
|
||||||
resolution: {integrity: sha512-CZmid9EyOi2DDGW3s/lQWNTNt1wPHUqTSDDPZqpC3mlou/m/N4e+kZxpDJ+LY1cmXyP7cPQO9H1tDlXesmItiw==}
|
resolution: {integrity: sha512-h6tndGnTLrAmfvxK5XsXGdZaMi2GBmFZBFiBqtabC7Y05A1ZSOaWg/d9f60Ydh586LmVonz5VGrhvzTdn5ukhg==}
|
||||||
|
|
||||||
'@folo-services/exceptions@0.1.10':
|
'@folo-services/exceptions@0.1.11':
|
||||||
resolution: {integrity: sha512-54ShG+RYYnXarjdxPyC1SSqIeAIbQimB/iBBRgt+LzHp9jMTenjU2CfP7RiYpgwCrS5tNz6eH+cNjZdWa6jTag==}
|
resolution: {integrity: sha512-OMKat8KcIxjw9pBfG3pC1kRcIs0YiuaYmMDA2MRy8AEGcki8F9u5e5QH7OKgP/LlNR7Fs2Csz1XetvcC4nB+RQ==}
|
||||||
|
|
||||||
'@folo-services/shared@0.0.7':
|
'@folo-services/shared@0.0.8':
|
||||||
resolution: {integrity: sha512-Tb94/bSFU2vXwg/JUSjeGFoDhPyv9NANa19P0L/ohNsEvymSXyjdmkpKowkw4+CQEhNiSkROTDjPJylO6nIm8g==}
|
resolution: {integrity: sha512-rfl1vvGMxUnchckGSKOkeT5hS1NAzZTJ8gPF6yFR+TuSe2SZzq1a10/66nK9OxmqaDFaHdOpQfNXz2vp0gRJpg==}
|
||||||
|
|
||||||
'@fontsource/sn-pro@5.2.5':
|
'@fontsource/sn-pro@5.2.5':
|
||||||
resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==}
|
resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==}
|
||||||
|
|
@ -18921,12 +18921,12 @@ snapshots:
|
||||||
|
|
||||||
'@floating-ui/utils@0.2.10': {}
|
'@floating-ui/utils@0.2.10': {}
|
||||||
|
|
||||||
'@follow-app/client-sdk@0.3.27(@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.29(@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:
|
dependencies:
|
||||||
'@folo-services/constants': 0.1.17
|
'@folo-services/constants': 0.1.18
|
||||||
'@folo-services/drizzle': 0.1.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)
|
'@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)
|
||||||
'@folo-services/exceptions': 0.1.10
|
'@folo-services/exceptions': 0.1.11
|
||||||
'@folo-services/shared': 0.0.7(@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.8(@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
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/client-rds-data'
|
- '@aws-sdk/client-rds-data'
|
||||||
|
|
@ -18960,10 +18960,10 @@ snapshots:
|
||||||
- sql.js
|
- sql.js
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
|
||||||
'@folo-services/ai-tools@0.2.19(@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.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)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/openai': 2.0.2(zod@3.25.76)
|
'@ai-sdk/openai': 2.0.2(zod@3.25.76)
|
||||||
'@folo-services/drizzle': 0.1.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)
|
'@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: 5.0.4(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)
|
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
|
||||||
|
|
@ -18999,13 +18999,13 @@ snapshots:
|
||||||
- sql.js
|
- sql.js
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
|
||||||
'@folo-services/constants@0.1.17':
|
'@folo-services/constants@0.1.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@folo-services/drizzle@0.1.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)':
|
'@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:
|
dependencies:
|
||||||
'@folo-services/exceptions': 0.1.10
|
'@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-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)
|
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
|
nanoid: 5.1.5
|
||||||
|
|
@ -19042,11 +19042,11 @@ snapshots:
|
||||||
- sql.js
|
- sql.js
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
|
||||||
'@folo-services/exceptions@0.1.10': {}
|
'@folo-services/exceptions@0.1.11': {}
|
||||||
|
|
||||||
'@folo-services/shared@0.0.7(@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.8(@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:
|
dependencies:
|
||||||
'@folo-services/drizzle': 0.1.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)
|
'@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)
|
||||||
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-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)
|
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
|
zod: 3.25.76
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,4 @@ overrides:
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
typescript: "5.8.3"
|
typescript: "5.8.3"
|
||||||
"@follow-app/client-sdk": "0.3.27"
|
"@follow-app/client-sdk": "0.3.29"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue