{
  "title": "Quick Chat",
  "description": "A ready-to-use chat interface powered by Chrome's native Gemini Nano with streaming responses",
  "name": "native-ai-quick-chat",
  "dependencies": [
    "react-markdown",
    "remark-gfm",
    "prompt-api-polyfill",
    "copy-to-clipboard"
  ],
  "registryDependencies": [
    "button",
    "dialog.tsx",
    "alert",
    "card",
    "tabs",
    "badge",
    "popover",
    "spinner",
    "input",
    "button.tsx",
    "spinner.tsx"
  ],
  "type": "registry:ui",
  "files": [
    {
      "path": "components/paceui/native-ai/quick-chat/index.tsx",
      "type": "registry:ui",
      "content": "import { useState } from \"react\";\n\nimport { AlertCircle, Bot, Smartphone } from \"lucide-react\";\n\nimport { useBrowserSupport } from \"@/hooks/native-ai/use-browser-support\";\nimport { useNativeAI } from \"@/hooks/native-ai/use-native-ai\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"@/components/ui/dialog.tsx\";\nimport { BrowserSupport } from \"@/components/paceui/native-ai/browser-support\";\nimport { ChatHeader } from \"@/components/paceui/native-ai/chat-header\";\nimport { ChatInput } from \"@/components/paceui/native-ai/chat-input\";\nimport { ChatMessages } from \"@/components/paceui/native-ai/chat-messages\";\n\nexport const QuickChat = () => {\n    const { isSupported, isMobile } = useBrowserSupport();\n    const [inputText, setInputText] = useState<string>(\"\");\n\n    const { isLoading, status, loadingPercentage, messages, sendMessage, stop, resetChat, contextInfo } = useNativeAI({\n        initialSystemPrompt: \"You are a helpful assistant\",\n    });\n\n    const handleSend = async () => {\n        if (!inputText.trim() || isLoading) return;\n\n        const query = inputText;\n        setInputText(\"\");\n        await sendMessage({ message: { content: query, role: \"user\" } });\n    };\n\n    return (\n        <div className=\"bg-card mx-auto flex h-full w-full max-w-2xl flex-col overflow-hidden rounded-md border\">\n            <div className=\"bg-muted/20 border-b px-4 py-3\">\n                <ChatHeader\n                    title=\"Quick Chat\"\n                    isSupported={isSupported}\n                    contextInfo={contextInfo}\n                    status={status}\n                    loadingPercentage={loadingPercentage}\n                    onReset={resetChat}\n                />\n            </div>\n\n            {!isSupported ? (\n                <div className=\"flex h-80 items-center justify-center\">\n                    <div className=\"m-3 flex flex-col items-center justify-center gap-3 rounded-md border border-dashed p-4 text-center sm:p-5\">\n                        <div className=\"flex items-center gap-2 font-medium\">\n                            {isMobile ? <Smartphone className=\"size-4.5\" /> : <AlertCircle className=\"size-4.5\" />}\n                            <p className=\"max-sm:text-sm\">\n                                {isMobile ? \"Mobile Devices Not Supported\" : \"Chrome Built-in AI Not Detected\"}\n                            </p>\n                        </div>\n                        {isMobile && (\n                            <p className=\"text-muted-foreground max-w-xs text-xs\">\n                                Native AI models are not supported on mobile devices yet. Please open this on a desktop\n                                browser.\n                            </p>\n                        )}\n                        <Dialog>\n                            <DialogTrigger\n                                render={\n                                    <Button className=\"\" variant=\"outline\">\n                                        How to Enable It\n                                    </Button>\n                                }></DialogTrigger>\n                            <DialogContent className=\"w-sm p-0 md:min-w-xl\">\n                                <BrowserSupport className=\"max-h-[80dvh]\" />\n                            </DialogContent>\n                        </Dialog>\n                    </div>\n                </div>\n            ) : (\n                <div className=\"flex min-h-0 flex-1 flex-col overflow-hidden\">\n                    <ChatMessages\n                        messages={messages}\n                        isLoading={isLoading}\n                        isSupported={isSupported}\n                        className=\"custom-scrollbar max-h-120 min-h-80 overflow-y-auto\"\n                        effect=\"fade-in\"\n                        emptyState={\n                            <div className=\"flex h-80 flex-col items-center justify-center p-6 text-center\">\n                                <div className=\"bg-muted rounded-md p-3\">\n                                    <Bot className=\"size-7\" />\n                                </div>\n                                <p className=\"mt-3 font-medium\">How can I help you today?</p>\n                                <p className=\"text-muted-foreground mt-1 max-w-xs text-sm\">\n                                    Ask a question to start local generation.\n                                </p>\n                            </div>\n                        }\n                    />\n                    <div className=\"border-t p-3\">\n                        <ChatInput\n                            value={inputText}\n                            onChange={setInputText}\n                            onSubmit={handleSend}\n                            onStop={stop}\n                            isLoading={isLoading}\n                            isSupported={isSupported}\n                        />\n                    </div>\n                </div>\n            )}\n        </div>\n    );\n};\n"
    },
    {
      "path": "hooks/native-ai/use-browser-support.ts",
      "type": "registry:ui",
      "content": "import { useCallback, useEffect, useState } from \"react\";\n\nexport type BrowserType = \"chrome\" | \"firefox\" | \"edge\" | \"safari\";\n\nexport const SUPPORTED_NATIVE_BROWSERS: BrowserType[] = [\"chrome\"];\n\nexport type AIEngine = \"chrome-native\" | \"polyfill-webllm\";\n\nexport function detectMobile(): boolean {\n    if (typeof window === \"undefined\" || !navigator?.userAgent) {\n        return false;\n    }\n\n    const ua = navigator.userAgent;\n    const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(ua);\n    const isCoarsePointer = typeof window.matchMedia === \"function\" && window.matchMedia(\"(pointer: coarse)\").matches;\n\n    return isMobileUA || (isCoarsePointer && \"ontouchstart\" in window);\n}\n\nexport function detectBrowser(): BrowserType {\n    if (typeof window === \"undefined\" || !navigator?.userAgent) {\n        return \"chrome\";\n    }\n\n    const ua = navigator.userAgent;\n    if (ua.includes(\"Edg/\")) return \"edge\";\n    if (ua.includes(\"Firefox\") || ua.includes(\"FxIOS\")) return \"firefox\";\n    if (ua.includes(\"Safari\") && !ua.includes(\"Chrome\") && !ua.includes(\"Chromium\")) return \"safari\";\n    if (ua.includes(\"Chrome\") || ua.includes(\"Chromium\")) return \"chrome\";\n\n    return \"chrome\";\n}\n\nexport const useBrowserSupport = () => {\n    const [isLoading, setIsLoading] = useState<boolean>(true);\n    const [isSupported, setIsSupported] = useState<boolean>(false);\n    const [isMobile, setIsMobile] = useState<boolean>(false);\n    const [engine, setEngine] = useState<AIEngine | null>(null);\n    const [errorReason, setErrorReason] = useState<string | undefined>(undefined);\n    const [detectedBrowser, setDetectedBrowser] = useState<BrowserType>(\"chrome\");\n    const [canEnable, setCanEnable] = useState<boolean>(false);\n\n    const checkSupport = useCallback(async () => {\n        setIsLoading(true);\n        const mobile = detectMobile();\n        setIsMobile(mobile);\n\n        const browser = detectBrowser();\n        setDetectedBrowser(browser);\n\n        if (mobile) {\n            setIsSupported(false);\n            setCanEnable(false);\n            setEngine(null);\n            setErrorReason(\n                \"Mobile devices are not supported yet. Native AI requires desktop browsers with hardware acceleration.\",\n            );\n            setIsLoading(false);\n            return;\n        }\n\n        const canBeEnabled = SUPPORTED_NATIVE_BROWSERS.includes(browser);\n        setCanEnable(canBeEnabled);\n\n        const globalLangModel =\n            typeof window !== \"undefined\" ? (window as any).LanguageModel || (window as any).ai?.languageModel : null;\n        const hasChromeAI = !!(globalLangModel && !globalLangModel.__isPolyfill);\n\n        if (hasChromeAI) {\n            setIsSupported(true);\n            setEngine(\"chrome-native\");\n            setErrorReason(undefined);\n        } else {\n            setIsSupported(false);\n            setEngine(null);\n            setErrorReason(\n                browser === \"chrome\"\n                    ? \"Chrome Built-in AI is not enabled or missing model flags in your browser.\"\n                    : `${browser.charAt(0).toUpperCase() + browser.slice(1)} does not currently support Chrome's native AI APIs. Use Chrome with AI flags.`,\n            );\n        }\n\n        setIsLoading(false);\n    }, []);\n\n    useEffect(() => {\n        checkSupport();\n    }, [checkSupport]);\n\n    return {\n        isLoading,\n        isSupported,\n        isMobile,\n        engine,\n        errorReason,\n        detectedBrowser,\n        canEnable,\n        recheck: checkSupport,\n    };\n};\n"
    },
    {
      "path": "hooks/native-ai/use-native-ai/index.ts",
      "type": "registry:ui",
      "content": "import { useRef, useState } from \"react\";\n\nimport { useNativeAiCore } from \"./core\";\nimport { filteredMessagesFn, parseTaskResultsFromContent, parseTasksBatch } from \"./helpers\";\nimport type { Message, Task, TaskResult, UseNativeAIProps } from \"./types\";\n\nexport const useNativeAI = (props: UseNativeAIProps) => {\n    const {\n        initialSystemPrompt = \"You are a helpful, concise AI assistant.\",\n        eagerLoaded = true,\n        autoCancelOnNextPrompt = true,\n    } = props;\n    const [taskResults, setTaskResults] = useState<TaskResult[]>([]);\n    const [messages, setMessages] = useState<Message[]>([{ role: \"system\", content: initialSystemPrompt }]);\n    const [isLoading, setIsLoading] = useState<boolean>(false);\n    const [loadingTasks, setLoadingTasks] = useState<Record<string, boolean>>({});\n\n    const abortControllerRef = useRef<AbortController | null>(null);\n    const taskAbortControllerRef = useRef<AbortController | null>(null);\n\n    const { getAdapter, syncContextInfo, contextInfo, isInitializing, isReady, status, loadingPercentage } =\n        useNativeAiCore({ initialSystemPrompt, eagerLoaded });\n\n    const onCompleted = (message: Message, id: TaskResult[\"id\"]) => {\n        const parsedAnswers = parseTaskResultsFromContent(message.content, id);\n        if (parsedAnswers) setTaskResults((prev) => [...prev, parsedAnswers]);\n    };\n\n    const stopTasks = () => {\n        if (taskAbortControllerRef.current) {\n            taskAbortControllerRef.current.abort();\n            taskAbortControllerRef.current = null;\n        }\n        setLoadingTasks({});\n    };\n\n    const stop = async () => {\n        if (abortControllerRef.current) {\n            abortControllerRef.current.abort();\n            abortControllerRef.current = null;\n        }\n        stopTasks();\n        const adapter = await getAdapter();\n        adapter?.stopStream?.();\n        setIsLoading(false);\n    };\n\n    const isTaskLoading = (taskId?: string): boolean => {\n        if (taskId) {\n            return Boolean(loadingTasks[taskId]);\n        }\n        return Object.values(loadingTasks).some(Boolean);\n    };\n\n    const getTaskResult = (taskId: string): TaskResult | undefined => {\n        return taskResults.find((res) => res.id === taskId);\n    };\n\n    const clearTaskResults = (taskId?: string) => {\n        if (taskId) {\n            setTaskResults((prev) => prev.filter((res) => res.id !== taskId));\n        } else {\n            setTaskResults([]);\n        }\n    };\n\n    const sendMessage = async ({ message, tasks }: { message: Message; tasks?: Task[] }) => {\n        const { content } = message;\n        if (!content.trim()) return;\n\n        const isAnyTaskActive = Object.values(loadingTasks).some(Boolean);\n        if (isLoading || isAnyTaskActive) {\n            if (autoCancelOnNextPrompt) {\n                await stop();\n            } else {\n                return;\n            }\n        }\n\n        const newMessages = [\n            ...messages,\n            { ...message },\n            { role: \"assistant\" as const, content: \"\", custom: message.custom },\n        ];\n        setMessages(newMessages);\n\n        const historyToSend = newMessages.slice(0, -1);\n        setIsLoading(true);\n\n        const controller = new AbortController();\n        abortControllerRef.current = controller;\n\n        let finalResponse = \"\";\n\n        try {\n            const adapter = await getAdapter();\n            if (!adapter) {\n                setMessages((prev) => {\n                    const updated = [...prev];\n                    const lastIndex = updated.length - 1;\n                    if (lastIndex >= 0 && updated[lastIndex].role === \"assistant\") {\n                        updated[lastIndex] = {\n                            ...updated[lastIndex],\n                            content: \"Failed to initialize local AI engine. It is unavailable.\",\n                        };\n                    }\n                    return updated;\n                });\n                return;\n            }\n\n            const cleanHistory = historyToSend.map(({ role, content }) => ({ role, content }));\n\n            await adapter.streamMessage(\n                cleanHistory,\n                (_delta, fullResponse) => {\n                    finalResponse = fullResponse;\n\n                    setMessages((prev) => {\n                        const updated = [...prev];\n                        const lastIndex = updated.length - 1;\n\n                        if (lastIndex >= 0 && updated[lastIndex].role === \"assistant\") {\n                            updated[lastIndex] = {\n                                ...updated[lastIndex],\n                                content: fullResponse,\n                            };\n                        }\n                        return updated;\n                    });\n                },\n                controller.signal,\n            );\n\n            syncContextInfo();\n        } catch (err: any) {\n            if (err?.name === \"AbortError\" || controller.signal.aborted) {\n                return;\n            }\n            console.error(\"AI Generation Error:\", err);\n            setMessages((prev) => {\n                const updated = [...prev];\n                const lastIndex = updated.length - 1;\n                if (lastIndex >= 0 && updated[lastIndex].role === \"assistant\" && !updated[lastIndex].content) {\n                    updated[lastIndex] = {\n                        ...updated[lastIndex],\n                        content: `Error: ${err instanceof Error ? err.message : String(err)}`,\n                    };\n                }\n                return updated;\n            });\n        } finally {\n            setIsLoading(false);\n            abortControllerRef.current = null;\n        }\n\n        if (tasks && tasks.length > 0 && !controller.signal.aborted) {\n            const taskController = new AbortController();\n            taskAbortControllerRef.current = taskController;\n\n            const initialTaskState: Record<string, boolean> = {};\n            tasks.forEach((t) => {\n                initialTaskState[t.id] = true;\n            });\n            setLoadingTasks(initialTaskState);\n\n            // Clear old results for these tasks\n            setTaskResults((prev) => prev.filter((res) => !tasks.some((t) => t.id === res.id)));\n\n            try {\n                const adapter = await getAdapter();\n                if (adapter && !taskController.signal.aborted) {\n                    const cleanHistory = historyToSend.map(({ role, content }) => ({ role, content }));\n                    const combinedPrompt = parseTasksBatch(tasks);\n                    const promptMessages = [\n                        ...cleanHistory,\n                        { role: \"assistant\", content: finalResponse },\n                        { role: \"user\", content: combinedPrompt },\n                    ];\n\n                    const combinedString = await adapter.isolatedPromptMessage(promptMessages);\n\n                    if (combinedString && !taskController.signal.aborted) {\n                        for (const t of tasks) {\n                            onCompleted({ content: combinedString, role: \"assistant\" }, t.id);\n                        }\n                    }\n                }\n            } catch (err) {\n                console.error(\"Side Task Execution Error:\", err);\n            } finally {\n                setLoadingTasks({});\n                taskAbortControllerRef.current = null;\n            }\n        }\n    };\n\n    const promptMessage = async (messagesToSend: Message[]) => {\n        const adapter = await getAdapter();\n        if (!adapter) return null;\n        return adapter.promptMessage(messagesToSend);\n    };\n\n    const isolatedPromptMessage = async (messagesToSend: Message[]) => {\n        const adapter = await getAdapter();\n        if (!adapter) return null;\n        return adapter.isolatedPromptMessage(messagesToSend);\n    };\n\n    const resetChat = async () => {\n        await stop();\n        setMessages([{ role: \"system\", content: initialSystemPrompt }]);\n        setTaskResults([]);\n        const adapter = await getAdapter();\n        if (adapter?.reset) {\n            await adapter.reset(initialSystemPrompt);\n        }\n        await syncContextInfo();\n    };\n\n    return {\n        sendMessage,\n        stop,\n        stopStream: stop,\n        resetChat,\n        reset: resetChat,\n        promptMessage,\n        isolatedPromptMessage,\n        messages,\n        isLoading,\n        isTaskLoading,\n        loadingTasks,\n        getTaskResult,\n        clearTaskResults,\n        isInitializing,\n        isReady,\n        status,\n        loadingPercentage,\n        filteredMessages: filteredMessagesFn(messages),\n        taskResults,\n        setTaskResults,\n        contextInfo,\n    };\n};\n"
    },
    {
      "path": "hooks/native-ai/use-native-ai/core/index.ts",
      "type": "registry:ui",
      "content": "import { useEffect, useRef, useState } from \"react\";\n\nimport { ChromeBuiltInAdapter } from \"./adapters/chrome-native\";\nimport type { AIContextInfo, AIEngineAdapter, UseNativeAICoreProps } from \"./types\";\n\nexport const useNativeAiCore = ({ initialSystemPrompt, eagerLoaded }: UseNativeAICoreProps) => {\n    const activeAdapterRef = useRef<AIEngineAdapter | null>(null);\n    const adapterPromiseRef = useRef<Promise<AIEngineAdapter | null> | null>(null);\n    const [engineType, setEngineType] = useState<string>(\"detecting\");\n    const [initProgress, setInitProgress] = useState<string>(\"\");\n    const [loadingPercentage, setLoadingPercentage] = useState<number | null>(null);\n    const [contextInfo, setContextInfo] = useState<AIContextInfo | null>(null);\n    const [isInitializing, setIsInitializing] = useState(false);\n    const [isReady, setIsReady] = useState(false);\n\n    const getAdapter = async (): Promise<AIEngineAdapter | null> => {\n        if (activeAdapterRef.current) return activeAdapterRef.current;\n        if (adapterPromiseRef.current) return adapterPromiseRef.current;\n\n        setIsInitializing(true);\n        let currentPromise: Promise<AIEngineAdapter | null> = null!;\n        currentPromise = (async () => {\n            try {\n                const chromeAdapter = new ChromeBuiltInAdapter();\n                if (await chromeAdapter.isAvailable()) {\n                    await chromeAdapter.init(initialSystemPrompt, (progress, pct) => {\n                        if (adapterPromiseRef.current === currentPromise) {\n                            setInitProgress(progress);\n                            if (typeof pct === \"number\") setLoadingPercentage(pct);\n                        }\n                    });\n                    if (adapterPromiseRef.current !== currentPromise) {\n                        chromeAdapter.destroy?.();\n                        return null;\n                    }\n                    activeAdapterRef.current = chromeAdapter;\n                    setEngineType(chromeAdapter.type);\n                    setIsReady(true);\n                    const info = await chromeAdapter.getAIContextInfo();\n                    setContextInfo(info);\n                    return chromeAdapter;\n                }\n\n                if (adapterPromiseRef.current === currentPromise) {\n                    adapterPromiseRef.current = null;\n                }\n                return null;\n            } catch (err) {\n                console.error(\"AI Adapter initialization error:\", err);\n                if (adapterPromiseRef.current === currentPromise) {\n                    adapterPromiseRef.current = null;\n                }\n                throw err;\n            } finally {\n                if (adapterPromiseRef.current === currentPromise) {\n                    setIsInitializing(false);\n                }\n            }\n        })();\n        adapterPromiseRef.current = currentPromise;\n\n        return adapterPromiseRef.current;\n    };\n\n    useEffect(() => {\n        if (eagerLoaded && !activeAdapterRef.current && !adapterPromiseRef.current) {\n            getAdapter();\n        }\n\n        return () => {\n            if (activeAdapterRef.current?.destroy) {\n                activeAdapterRef.current.destroy();\n                activeAdapterRef.current = null;\n            }\n            adapterPromiseRef.current = null;\n        };\n    }, [eagerLoaded]);\n\n    const syncContextInfo = async () => {\n        const adapter = activeAdapterRef.current;\n        if (!adapter) {\n            setContextInfo(null);\n            return;\n        }\n        try {\n            const info = await adapter.getAIContextInfo();\n            setContextInfo(info);\n        } catch {\n            setContextInfo(null);\n        }\n    };\n\n    const status: \"loading\" | \"ready\" | \"lazy\" = isInitializing ? \"loading\" : isReady ? \"ready\" : \"lazy\";\n\n    return {\n        getAdapter,\n        engineType,\n        initProgress,\n        loadingPercentage,\n        contextInfo,\n        syncContextInfo,\n        isInitializing,\n        isReady,\n        status,\n    };\n};\n\nexport const useNativeAi = useNativeAiCore;\n"
    },
    {
      "path": "hooks/native-ai/use-native-ai/core/adapters/chrome-native.ts",
      "type": "registry:ui",
      "content": "import type { AIContextInfo, AIEngineAdapter } from \"../types\";\n\ndeclare global {\n    interface Window {\n        LanguageModel?: LanguageModelFactory;\n        ai?: {\n            languageModel?: LanguageModelFactory;\n        };\n    }\n}\n\ninterface LanguageModelFactory {\n    availability?: (options?: Record<string, any>) => Promise<\"readily\" | \"after-download\" | \"no\">;\n    capabilities?: () => Promise<{ available: \"readily\" | \"after-download\" | \"no\" }>;\n    create: (options?: {\n        systemPrompt?: string;\n        initialPrompts?: Array<{ role: \"system\" | \"user\" | \"assistant\"; content: string }>;\n        monitor?: (m: EventTarget) => void;\n        topK?: number;\n        temperature?: number;\n        signal?: AbortSignal;\n        expectedInputs?: Array<{\n            type: \"text\" | \"image\" | \"audio\";\n            languages?: string[];\n        }>;\n        expectedOutputs?: Array<{\n            type: \"text\";\n            languages?: string[];\n        }>;\n    }) => Promise<ChromeAISession>;\n}\n\ninterface ChromeAISession {\n    contextUsage: number;\n    contextWindow: number;\n    promptStreaming: (\n        prompt: string | Array<{ role: string; content: string }>,\n    ) => AsyncIterable<string> | ReadableStream<string>;\n    prompt: (prompt: string) => Promise<string>;\n    destroy: () => void;\n    clone: () => Promise<ChromeAISession>;\n}\n\nexport class ChromeBuiltInAdapter implements AIEngineAdapter {\n    type = \"chrome-native\";\n    private session: ChromeAISession | null = null;\n    private currentAbortController: AbortController | null = null;\n\n    async isAvailable(): Promise<boolean> {\n        const factory = this.getFactory();\n        if (!factory) return false;\n\n        try {\n            if (typeof factory.availability === \"function\") {\n                const status = await factory.availability();\n                return status !== \"no\";\n            }\n            if (typeof factory.capabilities === \"function\") {\n                const caps = await factory.capabilities();\n                return caps.available !== \"no\";\n            }\n            return true;\n        } catch {\n            return false;\n        }\n    }\n\n    async init(systemPrompt: string, onProgress?: (progress: string, percentage?: number) => void): Promise<void> {\n        if (this.session) return;\n\n        const factory = this.getFactory();\n        if (!factory) {\n            throw new Error(\"Chrome AI LanguageModel is not supported in this browser.\");\n        }\n\n        onProgress?.(\"Initializing Chrome Native Gemini Nano...\");\n\n        this.session = await factory.create({\n            systemPrompt,\n            initialPrompts: [{ role: \"system\", content: systemPrompt }],\n            temperature: 0,\n            topK: 1,\n            monitor(m: EventTarget) {\n                m.addEventListener(\"downloadprogress\", (e: any) => {\n                    if (e && typeof e.loaded === \"number\" && typeof e.total === \"number\" && e.total > 0) {\n                        const pct = Math.min(100, Math.max(0, Math.round((e.loaded / e.total) * 100)));\n                        onProgress?.(`Downloading model (${pct}%)...`, pct);\n                    } else if (e && typeof e.percentage === \"number\" && e.percentage > 0) {\n                        const pct = Math.min(100, Math.max(0, Math.round(e.percentage)));\n                        onProgress?.(`Downloading model (${pct}%)...`, pct);\n                    }\n                });\n            },\n        });\n\n        onProgress?.(\"Model ready\", 100);\n    }\n\n    async streamBaseMessage(\n        session: ChromeAISession | null,\n        userText: string,\n        onChunk: (delta: string, fullResponse: string) => void,\n        signal?: AbortSignal,\n    ): Promise<void> {\n        if (!session) throw new Error(\"Session not initialized\");\n        const stream = session.promptStreaming(userText);\n        let fullResponse = \"\";\n\n        try {\n            if (Symbol.asyncIterator in stream) {\n                for await (const chunk of stream as AsyncIterable<string>) {\n                    if (signal?.aborted || this.currentAbortController?.signal.aborted) {\n                        break;\n                    }\n                    this.processChunk(chunk, fullResponse, (delta, newFull) => {\n                        fullResponse = newFull;\n                        onChunk(delta, fullResponse);\n                    });\n                }\n                // @ts-ignore\n            } else if (stream instanceof ReadableStream) {\n                const reader = (stream as ReadableStream<string>).getReader();\n                while (true) {\n                    if (signal?.aborted || this.currentAbortController?.signal.aborted) {\n                        await reader.cancel();\n                        break;\n                    }\n                    const { done, value } = await reader.read();\n                    if (done) break;\n                    if (value) {\n                        this.processChunk(value, fullResponse, (delta, newFull) => {\n                            fullResponse = newFull;\n                            onChunk(delta, fullResponse);\n                        });\n                    }\n                }\n            }\n        } catch (err: any) {\n            if (err?.name === \"AbortError\" || signal?.aborted) {\n                return;\n            }\n            throw err;\n        }\n    }\n\n    async streamMessage(\n        messages: any[],\n        onChunk: (delta: string, fullResponse: string) => void,\n        signal?: AbortSignal,\n    ): Promise<void> {\n        this.currentAbortController = new AbortController();\n        const userText = messages[messages.length - 1]?.content || \"\";\n\n        const handleAbort = () => {\n            if (this.currentAbortController) {\n                this.currentAbortController.abort();\n            }\n        };\n\n        if (signal) {\n            signal.addEventListener(\"abort\", handleAbort);\n        }\n\n        try {\n            await this.streamBaseMessage(this.session, userText, onChunk, signal);\n        } finally {\n            if (signal) {\n                signal.removeEventListener(\"abort\", handleAbort);\n            }\n            this.currentAbortController = null;\n        }\n    }\n\n    stopStream(): void {\n        if (this.currentAbortController) {\n            this.currentAbortController.abort();\n            this.currentAbortController = null;\n        }\n    }\n\n    async promptMessage(messages: any[], sessionOverride?: ChromeAISession | null): Promise<string | null> {\n        const session = sessionOverride ?? this.session;\n        if (!session) return null;\n        const userText = messages[messages.length - 1]?.content || \"\";\n        return await session.prompt(userText);\n    }\n\n    async isolatedPromptMessage(messages: any[]): Promise<string | null> {\n        const clonedSession = await this.session?.clone();\n        if (!clonedSession) return null;\n\n        try {\n            return await this.promptMessage(messages, clonedSession);\n        } finally {\n            clonedSession.destroy();\n        }\n    }\n\n    async destroy(): Promise<void> {\n        if (this.session?.destroy) {\n            this.session.destroy();\n        }\n        this.session = null;\n    }\n\n    async reset(systemPrompt?: string): Promise<void> {\n        this.stopStream();\n        if (this.session?.destroy) {\n            try {\n                this.session.destroy();\n            } catch {}\n        }\n        this.session = null;\n        if (systemPrompt) {\n            await this.init(systemPrompt);\n        }\n    }\n\n    private getFactory(): LanguageModelFactory | null {\n        if (typeof window === \"undefined\") return null;\n        return window.LanguageModel || window.ai?.languageModel || null;\n    }\n\n    private processChunk(\n        chunk: string,\n        currentFullResponse: string,\n        update: (delta: string, newFullResponse: string) => void,\n    ): void {\n        let delta = \"\";\n        let nextFullResponse = \"\";\n\n        if (chunk.startsWith(currentFullResponse)) {\n            delta = chunk.slice(currentFullResponse.length);\n            nextFullResponse = chunk;\n        } else {\n            delta = chunk;\n            nextFullResponse = currentFullResponse + chunk;\n        }\n\n        update(delta, nextFullResponse);\n    }\n\n    async getAIContextInfo(): Promise<AIContextInfo | null> {\n        if (!this.session) return null;\n        return {\n            used: this.session.contextUsage,\n            window: this.session.contextWindow,\n        };\n    }\n}\n"
    },
    {
      "path": "hooks/native-ai/use-native-ai/core/types.ts",
      "type": "registry:ui",
      "content": "export type AIContextInfo = {\n    used: number;\n    window: number;\n};\n\nexport type UseNativeAICoreProps = {\n    initialSystemPrompt: string;\n    eagerLoaded?: boolean;\n};\n\nexport type AIEngineAdapter = {\n    type: string;\n    isAvailable: () => Promise<boolean>;\n    init: (systemPrompt: string, onProgress?: (progress: string, percentage?: number) => void) => Promise<void>;\n    streamMessage: (\n        messages: any[],\n        onChunk: (delta: string, fullResponse: string) => void,\n        signal?: AbortSignal,\n    ) => Promise<void>;\n    stopStream?: () => void;\n    getAIContextInfo: () => Promise<AIContextInfo | null>;\n    promptMessage: (messages: any[]) => Promise<string | null>;\n    isolatedPromptMessage: (messages: any[]) => Promise<string | null>;\n    reset?: (systemPrompt?: string) => Promise<void>;\n    destroy?: () => void | Promise<void>;\n};\n"
    },
    {
      "path": "hooks/native-ai/use-native-ai/helpers.ts",
      "type": "registry:ui",
      "content": "import type { Message, Task, TaskResult } from \"./types\";\n\nconst taskTypes: Record<Task[\"type\"], string> = {\n    single: `<answer>[Single Answer]</answer>`,\n    multiple: `<answer>[Answer 1]</answer>\\n                <answer>[Answer 2]</answer>`,\n};\n\nexport const filteredMessagesFn = (messages: Message[]): Message[] => {\n    return messages\n        .filter((m) => m.role !== \"system\")\n        .filter((m) => !m.custom)\n        .map((msg) => ({\n            ...msg,\n            content: msg.content.replace(/<custom-answer-context[\\s\\S]*?(?:<\\/custom-answer-context>|$)/g, \"\").trim(),\n        }));\n};\n\nexport const parseTaskResultsFromContent = (content: string, id: string): TaskResult | null => {\n    const contextRegex = new RegExp(\n        `<output-context[^>]*id=[\"']?${id}[\"']?[^>]*>([\\\\s\\\\S]*?)(?:<\\\\/output-context>|$)`,\n        \"i\",\n    );\n    const contextMatch = contextRegex.exec(content);\n\n    if (!contextMatch) {\n        return null;\n    }\n\n    const contextBody = contextMatch[1];\n    const answerRegex = /<answer[\\s\\S]*?>([\\s\\S]*?)(?:<\\/answer>|$)/g;\n    const answers: string[] = [];\n    let answerMatch: RegExpExecArray | null;\n\n    while ((answerMatch = answerRegex.exec(contextBody)) !== null) {\n        const trimmedValue = answerMatch[1].trim();\n        if (trimmedValue) {\n            answers.push(trimmedValue);\n        }\n    }\n\n    if (answers.length === 0) {\n        return null;\n    }\n\n    return {\n        id,\n        value: answers.length === 1 ? answers[0] : answers,\n    };\n};\n\nexport const parseTasksBatch = (tasks: Task[]): string => {\n    let combinedPrompt = \"Answer the following requests based exclusively on the conversation:\\n\\n\";\n    tasks.forEach((t) => {\n        combinedPrompt += `[Request ID: ${t.id}]\\n${t.prompt}\\n`;\n        const structure = taskTypes[t.type];\n        combinedPrompt += `Output format for this request:\\n<output-context id=\"${t.id}\">\\n${structure}\\n</output-context>\\n\\n`;\n    });\n    combinedPrompt += \"Output only the XML tags requested. Do not output any other text.\";\n    return combinedPrompt;\n};\n\nexport const createSystemContext = (context: string) => {\n    return `# SYSTEM INSTRUCTION\nYou are a single-topic assistant. Answer ONLY questions directly covered in the provided topic context.\n\nSTRICT RULES:\n1. OFF-TOPIC REJECTION: If a user asks an off-topic or general question, refuse to answer and state: \"Please ask me about topic, not general questions.\"\n2. NO OUTSIDE KNOWLEDGE: Base answers strictly on the given context. Do not make assumptions or add unmentioned details.\n3. CONCISE RESPONSES: Keep all answers brief and direct. Elaborate only if the user explicitly asks for more detail.\n\n# Topic Context:\n${context} `;\n};\n"
    },
    {
      "path": "hooks/native-ai/use-native-ai/types.ts",
      "type": "registry:ui",
      "content": "import type { UseNativeAICoreProps } from \"./core/types\";\n\nexport type UseNativeAIProps = UseNativeAICoreProps & {\n    /**\n     * If true, automatically cancels any active main message stream or side tasks when a new user message is submitted.\n     * @default true\n     */\n    autoCancelOnNextPrompt?: boolean;\n};\n\nexport type Message = {\n    role: \"system\" | \"user\" | \"assistant\";\n    content: string;\n    custom?: boolean;\n};\n\nexport type Task = {\n    id: string;\n    type: \"single\" | \"multiple\";\n    prompt: string;\n};\n\nexport type TaskResult = {\n    id: string;\n    value: string[] | string;\n};\n"
    },
    {
      "path": "components/paceui/native-ai/browser-support/index.tsx",
      "type": "registry:ui",
      "content": "import { type ReactNode, useState } from \"react\";\n\nimport copy from \"copy-to-clipboard\";\nimport { Check, CheckCircle2, Copy, Cpu, Loader2, RefreshCw, Smartphone } from \"lucide-react\";\n\nimport { SUPPORTED_NATIVE_BROWSERS, useBrowserSupport } from \"@/hooks/native-ai/use-browser-support\";\nimport { cn } from \"@/lib/utils.ts\";\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\n\ntype SupportedBrowser = \"chrome\" | \"firefox\" | \"edge\" | \"safari\";\n\nconst BROWSER_INSTRUCTIONS: Record<SupportedBrowser, ReactNode[]> = {\n    chrome: [\n        <>\n            <CopyableCode>chrome://settings/system</CopyableCode>: Ensure Hardware Acceleration\n        </>,\n        <>\n            <CopyableCode>chrome://flags/#prompt-api-for-gemini-nano</CopyableCode>: Set it to{\" \"}\n            <span className=\"font-medium\">Enabled</span>.\n        </>,\n        <>\n            <CopyableCode>chrome://flags/#optimization-guide-on-device-model</CopyableCode>: Set it to{\" \"}\n            <span className=\"font-medium\">Enabled BypassPerfRequirement</span>.\n        </>,\n        <>\n            <CopyableCode>chrome://components</CopyableCode>: Local{\" \"}\n            <span className=\"text-foreground font-medium\">Optimization Guide On Device Model</span>, and click{\" \"}\n            <span className=\"text-foreground font-medium\">Check for update</span> to finish downloading the local model.\n        </>,\n        <>\n            Click <span className=\"text-foreground font-medium\">Relaunch</span> to restart Chrome.\n        </>,\n    ],\n    firefox: [\n        <>\n            Open <CopyableCode>about:config</CopyableCode> in your address bar and accept the warning.\n        </>,\n        <>\n            Search for <span className=\"font-medium\">dom.webgpu.enabled</span> and set it to{\" \"}\n            <span className=\"text-foreground font-medium\">true</span>.\n        </>,\n        <>\n            Search for <span className=\"font-medium\">gfx.webrender.all</span> and ensure it is set to{\" \"}\n            <span className=\"text-foreground font-medium\">true</span> for GPU acceleration.\n        </>,\n        <>Restart Firefox to enable WebGPU hardware-accelerated local AI execution.</>,\n    ],\n    edge: [\n        <>\n            Open <CopyableCode>edge://settings/system</CopyableCode> and ensure{\" \"}\n            <span className=\"text-foreground font-medium\">Use graphics acceleration when available</span> is ON.\n        </>,\n        <>\n            Navigate to <CopyableCode>edge://flags/#enable-webgpu-developer-features</CopyableCode> and set to{\" \"}\n            <span className=\"text-foreground font-medium\">Enabled</span>.\n        </>,\n        <>\n            Optional (Edge Dev/Canary): Open <CopyableCode>edge://flags/#prompt-api-for-gemini-nano</CopyableCode> and\n            set to <span className=\"text-foreground font-medium\">Enabled</span>.\n        </>,\n        <>\n            Click <span className=\"text-foreground font-medium\">Restart</span> to reload Edge and activate local model\n            execution.\n        </>,\n    ],\n    safari: [\n        <>\n            Open Safari <span className=\"font-medium\">Settings</span> (\n            <kbd className=\"bg-muted/80 rounded border px-1.5 py-0.5 font-mono text-xs\">⌘ ,</kbd>) and switch to the{\" \"}\n            <span className=\"text-foreground font-medium\">Advanced</span> tab.\n        </>,\n        <>\n            Check <span className=\"text-foreground font-medium\">\"Show features for web developers\"</span> at the bottom.\n        </>,\n        <>\n            In the top menu bar, open <span className=\"text-foreground font-medium\">Develop &gt; Feature Flags</span>{\" \"}\n            and ensure <span className=\"text-foreground font-medium\">WebGPU</span> is checked.\n        </>,\n        <>Reload the page to execute models locally on Apple Silicon / Metal GPU.</>,\n    ],\n};\n\nfunction CopyableCode({ children }: { children: string }) {\n    const [copied, setCopied] = useState(false);\n\n    const handleCopy = async () => {\n        const success = await copy(children);\n        if (success) {\n            setCopied(true);\n            setTimeout(() => setCopied(false), 2000);\n        }\n    };\n\n    return (\n        <Button\n            type=\"button\"\n            onClick={handleCopy}\n            title=\"Click to copy URL\"\n            size=\"sm\"\n            className=\"gap-1.5 px-2.5\"\n            variant=\"secondary\">\n            <span>{children}</span>\n            {copied ? (\n                <Check className=\"size-3.5 shrink-0 text-emerald-500\" />\n            ) : (\n                <Copy className=\"size-3.5 shrink-0 opacity-60 transition-opacity hover:opacity-100\" />\n            )}\n        </Button>\n    );\n}\n\nfunction BrowserTabContent({ browser }: { browser: SupportedBrowser }) {\n    const isSupported = SUPPORTED_NATIVE_BROWSERS.includes(browser);\n\n    if (!isSupported) {\n        return (\n            <div className=\"text-muted-foreground rounded-md border border-dashed p-6 text-center text-sm\">\n                This browser is not yet supported natively. Please use a supported browser like Chrome.\n            </div>\n        );\n    }\n\n    return (\n        <ol className=\"list-inside list-decimal space-y-3 text-sm leading-relaxed\">\n            {BROWSER_INSTRUCTIONS[browser].map((step, idx) => (\n                <li key={idx}>{step}</li>\n            ))}\n        </ol>\n    );\n}\n\nexport const BrowserSupport = ({ className }: { className?: string }) => {\n    const { isLoading, isSupported, isMobile, engine, errorReason, detectedBrowser, recheck } = useBrowserSupport();\n    const browsers: SupportedBrowser[] = [\"chrome\", \"firefox\", \"edge\", \"safari\"];\n\n    if (isLoading) {\n        return (\n            <Card className=\"mx-auto my-8 w-full max-w-2xl p-8 text-center shadow-md\">\n                <Loader2 className=\"text-muted-foreground mx-auto h-8 w-8 animate-spin\" />\n                <p className=\"text-muted-foreground mt-2 text-sm\">Checking browser AI support...</p>\n            </Card>\n        );\n    }\n\n    return (\n        <Card className={cn(\"flex flex-col border-none shadow-none\", className)}>\n            <CardHeader>\n                <CardTitle className=\"text-lg\">Native AI Browser Support</CardTitle>\n                <CardDescription>\n                    {isSupported\n                        ? engine === \"polyfill-webllm\"\n                            ? \"Your browser is ready with WebGPU local AI fallback processing.\"\n                            : \"Your browser is successfully configured for Chrome native AI processing.\"\n                        : isMobile\n                          ? \"Mobile devices are not supported yet. Please switch to a desktop browser.\"\n                          : \"This application requires built-in local AI or WebGPU hardware acceleration on your device.\"}\n                </CardDescription>\n            </CardHeader>\n\n            <CardContent className=\"flex min-h-0 grow flex-col\">\n                <Alert variant={isMobile ? \"destructive\" : \"default\"}>\n                    {isSupported ? (\n                        <CheckCircle2 className=\"size-4.5\" />\n                    ) : isMobile ? (\n                        <Smartphone className=\"size-4\" />\n                    ) : (\n                        <Cpu className=\"size-4\" />\n                    )}\n                    <AlertTitle className=\"font-medium\">\n                        {isSupported\n                            ? \"You're ready to go!\"\n                            : isMobile\n                              ? \"Mobile Devices Not Supported\"\n                              : \"AI Support Check Failed\"}\n                    </AlertTitle>\n                    <AlertDescription className=\"mt-1 text-xs leading-relaxed opacity-90\">\n                        {isSupported\n                            ? engine === \"polyfill-webllm\"\n                                ? \"WebGPU Local AI Polyfill is active on your browser. AI tasks run 100% on your device.\"\n                                : \"Chrome Built-in AI is active on your device. You can start typing and generating text locally.\"\n                            : errorReason ||\n                              \"Your browser does not currently support local AI APIs or WebGPU acceleration.\"}\n                    </AlertDescription>\n                </Alert>\n\n                <div className=\"custom-scrollbar grow overflow-y-auto\">\n                    <Tabs defaultValue={detectedBrowser} className=\"mt-4 w-full\">\n                        <TabsList className=\"bg-muted/60 p-1\">\n                            {browsers.map((browser) => (\n                                <TabsTrigger\n                                    key={browser}\n                                    value={browser}\n                                    className=\"font-medium capitalize shadow-none\">\n                                    {browser}\n                                </TabsTrigger>\n                            ))}\n                        </TabsList>\n\n                        {browsers.map((browser) => (\n                            <TabsContent key={browser} value={browser} className=\"mt-3\">\n                                <BrowserTabContent browser={browser} />\n                            </TabsContent>\n                        ))}\n                    </Tabs>\n                </div>\n            </CardContent>\n\n            <CardFooter className=\"flex justify-end px-6\">\n                <Button onClick={recheck} variant=\"outline\" className=\"gap-2.5\">\n                    <RefreshCw className=\"size-4\" />\n                    Re-check Support\n                </Button>\n            </CardFooter>\n        </Card>\n    );\n};\n\nexport { BrowserSupport as SetupCard, BrowserSupport as NativeAiBrowserSupport };\n"
    },
    {
      "path": "components/paceui/native-ai/chat-header/index.tsx",
      "type": "registry:ui",
      "content": "import type { ReactNode } from \"react\";\n\nimport { RotateCcw, Sparkles } from \"lucide-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\nexport type AIStatus = \"loading\" | \"ready\" | \"lazy\" | string;\n\nexport interface ContextInfo {\n    used: number;\n    window: number;\n}\n\nexport interface ChatHeaderProps {\n    title?: string;\n    logo?: ReactNode;\n    icon?: ReactNode;\n    isSupported?: boolean;\n    contextInfo?: ContextInfo | null;\n    status?: AIStatus;\n    loadingPercentage?: number | null;\n    onReset?: () => void;\n}\n\ninterface ContextProgressRingProps {\n    used: number;\n    window: number;\n    size?: number;\n    strokeWidth?: number;\n}\n\nexport function ContextProgressRing({\n    used,\n    window: contextWindow,\n    size = 18,\n    strokeWidth = 2.5,\n}: ContextProgressRingProps) {\n    const percentage = contextWindow ? Math.min(100, Math.max(0, Math.round((used / contextWindow) * 100))) : 0;\n\n    const radius = (size - strokeWidth) / 2;\n    const circumference = 2 * Math.PI * radius;\n    const strokeDashoffset = circumference - (percentage / 100) * circumference;\n\n    return (\n        <Popover>\n            <PopoverTrigger\n                openOnHover\n                render={\n                    <button\n                        type=\"button\"\n                        aria-label=\"View context window usage\"\n                        className=\"relative inline-flex cursor-pointer items-center justify-center focus:outline-none\">\n                        <svg width={size} height={size} className=\"-rotate-90 transform\">\n                            <circle\n                                cx={size / 2}\n                                cy={size / 2}\n                                r={radius}\n                                className=\"stroke-muted-foreground/20\"\n                                strokeWidth={strokeWidth}\n                                fill=\"transparent\"\n                            />\n                            <circle\n                                cx={size / 2}\n                                cy={size / 2}\n                                r={radius}\n                                className=\"stroke-primary transition-all duration-300 ease-in-out\"\n                                strokeWidth={strokeWidth}\n                                strokeDasharray={circumference}\n                                strokeDashoffset={strokeDashoffset}\n                                strokeLinecap=\"round\"\n                                fill=\"transparent\"\n                            />\n                        </svg>\n                    </button>\n                }\n            />\n            <PopoverContent side=\"bottom\" align=\"center\" className=\"w-auto p-3 text-xs\">\n                <div className=\"space-y-1.5\">\n                    <p className=\"font-medium\">Context Window Usage ({percentage}%)</p>\n                    <p className=\"font-mono opacity-85\">\n                        {used.toLocaleString()} / {contextWindow.toLocaleString()} tokens\n                    </p>\n                </div>\n            </PopoverContent>\n        </Popover>\n    );\n}\n\nexport const ChatHeader = ({\n    title = \"AI Assistant\",\n    logo,\n    icon,\n    isSupported = false,\n    contextInfo,\n    status = \"ready\",\n    loadingPercentage,\n    onReset,\n}: ChatHeaderProps) => {\n    const renderIcon = logo ?? icon ?? <Sparkles className=\"size-5\" />;\n    const normalizedStatus = typeof status === \"string\" ? status.toLowerCase() : \"ready\";\n\n    return (\n        <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center gap-3\">\n                <div className=\"bg-muted flex items-center justify-center rounded-md p-2\">{renderIcon}</div>\n                <p className=\"font-medium\">{title}</p>\n            </div>\n\n            <div className=\"flex items-center gap-1.5\">\n                {contextInfo && <ContextProgressRing used={contextInfo.used} window={contextInfo.window} />}\n                {onReset && (\n                    <Popover>\n                        <PopoverTrigger\n                            render={\n                                <Button\n                                    variant=\"ghost\"\n                                    size=\"icon-sm\"\n                                    className=\"group\"\n                                    onClick={onReset}\n                                    aria-label=\"Reset chat\">\n                                    <RotateCcw className=\"size-4 transition-all duration-300 group-hover:-rotate-30\" />\n                                </Button>\n                            }\n                        />\n                        <PopoverContent side=\"bottom\" align=\"center\" className=\"w-auto p-2 text-xs\">\n                            Reset chat\n                        </PopoverContent>\n                    </Popover>\n                )}\n                {isSupported && (\n                    <div className=\"ms-auto flex items-center gap-2\">\n                        {normalizedStatus === \"loading\" && (\n                            <Badge variant=\"outline\" className=\"h-6.5 gap-1.5 px-2.5\">\n                                <Spinner className=\"size-3\" />\n                                Loading\n                                {typeof loadingPercentage === \"number\" && loadingPercentage > 0\n                                    ? ` ${loadingPercentage}%`\n                                    : \"\"}\n                            </Badge>\n                        )}\n                        {normalizedStatus === \"ready\" && (\n                            <Badge variant=\"outline\" className=\"h-6.5 gap-1.5 px-2.5\">\n                                <span className=\"bg-primary size-1.5 rounded-full\" />\n                                Ready\n                            </Badge>\n                        )}\n                        {normalizedStatus === \"lazy\" && (\n                            <Badge variant=\"outline\" className=\"h-6.5 gap-1.5 px-2.5\">\n                                <span className=\"size-1.5 rounded-full\" />\n                                Lazy\n                            </Badge>\n                        )}\n                        {![\"loading\", \"ready\", \"lazy\"].includes(normalizedStatus) && (\n                            <Badge variant=\"outline\" className=\"h-6.5 gap-1.5 px-2.5\">\n                                {status}\n                            </Badge>\n                        )}\n                    </div>\n                )}\n            </div>\n        </div>\n    );\n};\n"
    },
    {
      "path": "components/paceui/native-ai/chat-input/index.tsx",
      "type": "registry:ui",
      "content": "import { type KeyboardEvent } from \"react\";\n\nimport { Loader2, Send, Square } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\n\nexport interface ChatInputProps {\n    value: string;\n    onChange: (value: string) => void;\n    onSubmit: () => void;\n    onStop?: () => void;\n    onFocus?: () => void;\n    isLoading?: boolean;\n    isSupported?: boolean;\n    placeholder?: string;\n}\n\nexport const ChatInput = ({\n    value,\n    onChange,\n    onSubmit,\n    onStop,\n    onFocus,\n    isLoading = false,\n    isSupported = false,\n    placeholder = \"Type your message...\",\n}: ChatInputProps) => {\n    const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n        if (e.key === \"Enter\" && !e.shiftKey) {\n            e.preventDefault();\n            if (isLoading && onStop) {\n                onStop();\n            } else {\n                onSubmit();\n            }\n        }\n    };\n\n    return (\n        <form\n            onSubmit={(e) => {\n                e.preventDefault();\n                if (isLoading && onStop) {\n                    onStop();\n                } else {\n                    onSubmit();\n                }\n            }}\n            className=\"flex items-center gap-2\">\n            <Input\n                value={value}\n                onChange={(e) => onChange(e.target.value)}\n                onFocus={onFocus}\n                onKeyDown={handleKeyDown}\n                placeholder={isSupported ? placeholder : \"AI disabled\"}\n                disabled={!isSupported || (isLoading && !onStop)}\n                className=\"grow shadow-none\"\n            />\n            {isLoading && onStop ? (\n                <Button\n                    size=\"icon\"\n                    variant=\"destructive\"\n                    onClick={onStop}\n                    title=\"Stop streaming\"\n                    aria-label=\"Stop streaming\">\n                    <Square />\n                </Button>\n            ) : (\n                <Button\n                    type=\"submit\"\n                    size=\"icon\"\n                    disabled={!isSupported || isLoading || !value.trim()}\n                    aria-label=\"Send message\">\n                    {isLoading ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Send className=\"h-4 w-4\" />}\n                </Button>\n            )}\n        </form>\n    );\n};\n"
    },
    {
      "path": "components/paceui/native-ai/chat-messages/index.tsx",
      "type": "registry:ui",
      "content": "import { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nimport { ArrowDown, BotIcon, UserIcon } from \"lucide-react\";\n\nimport type { Message } from \"@/hooks/native-ai/use-native-ai/types.ts\";\nimport { cn } from \"@/lib/utils.ts\";\n\nimport { Button } from \"@/components/ui/button.tsx\";\nimport { type ResponseEffectType, ResponseRenderer } from \"@/components/paceui/native-ai/response-renderer\";\nimport { Spinner } from \"@/components/ui/spinner.tsx\";\n\nexport interface ChatMessagesProps {\n    messages: Message[];\n    isLoading?: boolean;\n    isSupported?: boolean;\n    emptyState?: ReactNode;\n    className?: string;\n    effect?: ResponseEffectType;\n}\n\nexport const ChatMessages = ({ messages, isSupported = false, emptyState, className, effect }: ChatMessagesProps) => {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const contentRef = useRef<HTMLDivElement>(null);\n\n    const [isAtBottom, setIsAtBottom] = useState(true);\n    const isAtBottomRef = useRef(true);\n    const prevMessagesLengthRef = useRef(0);\n\n    const chatMessages = messages.filter((m) => m.role !== \"system\");\n\n    const checkIfAtBottom = () => {\n        const container = containerRef.current;\n        if (!container) return true;\n\n        return container.scrollHeight - container.scrollTop - container.clientHeight <= 80;\n    };\n\n    const scrollToBottom = (smooth = true) => {\n        const container = containerRef.current;\n        if (!container) return;\n        container.scrollTo({\n            top: container.scrollHeight,\n            behavior: smooth ? \"smooth\" : \"auto\",\n        });\n        setIsAtBottom(true);\n        isAtBottomRef.current = true;\n    };\n\n    const handleScroll = () => {\n        const atBottom = checkIfAtBottom();\n        setIsAtBottom(atBottom);\n        isAtBottomRef.current = atBottom;\n    };\n\n    useEffect(() => {\n        const currentLength = chatMessages.length;\n        const isNewMessageAdded = currentLength > prevMessagesLengthRef.current;\n        prevMessagesLengthRef.current = currentLength;\n\n        const isLastUser = chatMessages[chatMessages.length - 1]?.role === \"user\";\n\n        if (isNewMessageAdded && isLastUser) {\n            scrollToBottom(true);\n        }\n    }, [chatMessages]);\n\n    useEffect(() => {\n        const container = containerRef.current;\n        const content = contentRef.current;\n        if (!container || !content) return;\n\n        const handleResize = () => {\n            if (isAtBottomRef.current) {\n                container.scrollTo({\n                    top: container.scrollHeight,\n                    behavior: \"auto\",\n                });\n            }\n        };\n\n        const observer = new ResizeObserver(handleResize);\n        observer.observe(content);\n\n        return () => observer.disconnect();\n    }, []);\n\n    return (\n        <div className=\"relative flex min-h-0 flex-col overflow-hidden\">\n            <div ref={containerRef} onScroll={handleScroll} className={className}>\n                {chatMessages.length === 0 && isSupported && emptyState}\n                <div ref={contentRef} className=\"space-y-4 p-4\">\n                    {chatMessages.map((message, index) => {\n                        const isUser = message.role === \"user\";\n                        return (\n                            <div key={index} className={cn(\"flex gap-3\", isUser ? \"justify-end\" : \"justify-start\")}>\n                                {!isUser && (\n                                    <div className=\"bg-card flex size-8 shrink-0 items-center justify-center rounded-md border\">\n                                        <BotIcon className=\"size-4.5\" />\n                                    </div>\n                                )}\n\n                                <div\n                                    className={cn(\n                                        \"max-w-[80%] rounded-md border px-4 py-2.5 text-sm\",\n                                        isUser ? \"bg-muted rounded-br-none\" : \"bg-card rounded-bl-none\",\n                                    )}>\n                                    {message.content ? (\n                                        <ResponseRenderer effect={effect} content={message.content} />\n                                    ) : (\n                                        <div className=\"text-muted-foreground flex items-center gap-2 py-0.5\">\n                                            <Spinner className=\"text-primary size-4\" />\n                                            <span className=\"text-xs\">Thinking...</span>\n                                        </div>\n                                    )}\n                                </div>\n\n                                {isUser && (\n                                    <div className=\"bg-muted flex size-8 shrink-0 items-center justify-center rounded-md border\">\n                                        <UserIcon className=\"size-4\" />\n                                    </div>\n                                )}\n                            </div>\n                        );\n                    })}\n                </div>\n            </div>\n\n            <Button\n                size=\"sm\"\n                variant=\"outline\"\n                onClick={() => scrollToBottom(true)}\n                aria-label=\"Scroll to bottom\"\n                className={cn(\n                    \"absolute -bottom-5 left-1/2 z-0 -translate-x-1/2 opacity-0 transition-all duration-300\",\n                    !isAtBottom && chatMessages.length > 0 && \"bottom-3 z-10 opacity-100\",\n                )}>\n                <ArrowDown className=\"size-3.5\" />\n                <span>Scroll to bottom</span>\n            </Button>\n        </div>\n    );\n};\n"
    },
    {
      "path": "components/paceui/native-ai/response-renderer/index.tsx",
      "type": "registry:ui",
      "content": "import React, { Children, type ReactNode, cloneElement, isValidElement, useMemo } from \"react\";\n\nimport { motion } from \"motion/react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\n\nexport type ResponseEffectType =\n    \"none\" | \"fade-in\" | \"fade-in-up\" | \"fade-in-down\" | \"blur-in\" | \"blur-in-up\" | \"shimmer\" | \"glow\";\n\nexport interface ResponseRendererProps {\n    content: string;\n    effect?: ResponseEffectType;\n    className?: string;\n}\n\ntype MDComponentProps = React.HTMLAttributes<HTMLElement> & { children?: ReactNode };\n\nconst EFFECT_CONFIGS: Record<string, any> = {\n    \"fade-in\": { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.25 } },\n    \"fade-in-up\": {\n        initial: { opacity: 0, y: 4 },\n        animate: { opacity: 1, y: 0 },\n        transition: { duration: 0.2, ease: \"easeOut\" },\n    },\n    \"fade-in-down\": {\n        initial: { opacity: 0, y: -4 },\n        animate: { opacity: 1, y: 0 },\n        transition: { duration: 0.2, ease: \"easeOut\" },\n    },\n    \"blur-in\": {\n        initial: { opacity: 0, filter: \"blur(3px)\", y: 2 },\n        animate: { opacity: 1, filter: \"blur(0px)\", y: 0 },\n        transition: { duration: 0.25, ease: \"easeOut\" },\n    },\n    \"blur-in-up\": {\n        initial: { opacity: 0, filter: \"blur(3.5px)\", y: 6 },\n        animate: { opacity: 1, filter: \"blur(0px)\", y: 0 },\n        transition: { duration: 0.25, ease: \"easeOut\" },\n    },\n    shimmer: {\n        initial: { opacity: 0, filter: \"brightness(1.8)\" },\n        animate: { opacity: 1, filter: \"brightness(1)\" },\n        transition: { duration: 0.3, ease: \"easeOut\" },\n    },\n    glow: {\n        initial: { opacity: 0, filter: \"drop-shadow(0px 0px 4px rgba(124, 58, 237, 0.6))\" },\n        animate: { opacity: 1, filter: \"drop-shadow(0px 0px 0px transparent)\" },\n        transition: { duration: 0.3, ease: \"easeOut\" },\n    },\n};\n\nconst AnimatedText = ({ children, effect = \"none\" }: { children: ReactNode; effect: ResponseEffectType }) => {\n    if (effect === \"none\" || !children) return <>{children}</>;\n\n    const animationConfig = EFFECT_CONFIGS[effect] || EFFECT_CONFIGS[\"fade-in\"];\n\n    const processNode = (node: ReactNode, index: number): ReactNode => {\n        if (typeof node === \"string\") {\n            return (\n                <span key={`wrap-${index}`} className=\"inline\">\n                    {(node.match(/(\\s+|\\S+)/g) || []).map((part, partIndex) =>\n                        /^\\s+$/.test(part) ? (\n                            <span key={`${index}-${partIndex}`}>{part}</span>\n                        ) : (\n                            <motion.span\n                                key={`${index}-${partIndex}`}\n                                className=\"inline-block origin-center whitespace-pre\"\n                                {...animationConfig}>\n                                {part}\n                            </motion.span>\n                        ),\n                    )}\n                </span>\n            );\n        }\n\n        if (isValidElement(node)) {\n            const element = node as React.ReactElement<{ children?: ReactNode }>;\n\n            if (element.type === \"code\" || element.type === \"pre\") return element;\n\n            if (element.props && element.props.children) {\n                return cloneElement(element, {\n                    key: element.key || `elem-${index}`,\n                    children: Children.map(element.props.children, processNode),\n                });\n            }\n        }\n        return node;\n    };\n\n    return <>{Children.map(children, processNode)}</>;\n};\n\nexport const ResponseRenderer = ({ content, effect = \"none\", className }: ResponseRendererProps) => {\n    const formattedContent = useMemo(() => formatStreamingMarkdown(content), [content]);\n\n    const components = useMemo(() => {\n        const withAnimation =\n            (Tag: React.ElementType, defaultClassName: string) =>\n            ({ children, ...props }: MDComponentProps) => (\n                <Tag className={defaultClassName} {...props}>\n                    <AnimatedText effect={effect}>{children}</AnimatedText>\n                </Tag>\n            );\n\n        return {\n            h1: withAnimation(\"h1\", \"mb-3 text-2xl font-medium\"),\n            h2: withAnimation(\"h2\", \"mb-2 text-xl font-medium\"),\n            h3: withAnimation(\"h3\", \"mb-1 text-lg font-medium\"),\n            ul: (props: MDComponentProps) => <ul className=\"mb-4 list-disc space-y-1 pl-6\" {...props} />,\n            li: withAnimation(\"li\", \"pl-1\"),\n            strong: withAnimation(\"span\", \"font-medium\"),\n            p: withAnimation(\"p\", \"mb-2 last:mb-0\"),\n            a: withAnimation(\"a\", \"text-primary underline underline-offset-4 transition-opacity hover:opacity-80\"),\n            table: (props: MDComponentProps) => (\n                <div className=\"my-4 w-full overflow-x-auto rounded-lg border\">\n                    <table className=\"w-full text-sm\" {...props} />\n                </div>\n            ),\n            thead: (props: MDComponentProps) => <thead className=\"bg-muted/50 border-b\" {...props} />,\n            tbody: (props: MDComponentProps) => <tbody className=\"[&_tr:last-child]:border-0\" {...props} />,\n            tr: (props: MDComponentProps) => <tr className=\"hover:bg-muted/50 border-b transition-colors\" {...props} />,\n            th: withAnimation(\"th\", \"text-muted-foreground h-10 px-4 text-left align-middle font-medium\"),\n            td: withAnimation(\"td\", \"p-4 align-middle\"),\n        };\n    }, [effect]);\n\n    return (\n        <div className={className}>\n            <ReactMarkdown remarkPlugins={[remarkGfm]} components={components as any}>\n                {formattedContent}\n            </ReactMarkdown>\n        </div>\n    );\n};\n\nexport function formatStreamingMarkdown(input: string): string {\n    if (!input) return \"\";\n    const trailingSpaces = input.match(/\\s+$/)?.[0] || \"\";\n    let str = trailingSpaces ? input.slice(0, -trailingSpaces.length) : input;\n    if (!str) return input;\n\n    str = str\n        .replace(/(?:^|\\s)(?:[*_~]+|`+|\\[|!\\[)$/, (match) => match.match(/^\\s+/)?.[0] || \"\")\n        .replace(/(?:^|\\n)(#{1,6})$/, \"\");\n    if (!str) return trailingSpaces;\n\n    let fence = \"\";\n    for (const line of str.split(\"\\n\")) {\n        const trimmedLine = line.trimStart();\n        if (trimmedLine.startsWith(\"```\") || trimmedLine.startsWith(\"~~~\")) {\n            fence = fence && trimmedLine.startsWith(fence) ? \"\" : fence || trimmedLine.slice(0, 3);\n        }\n    }\n    if (fence) return str + (str.endsWith(\"\\n\") ? \"\" : \"\\n\") + fence + trailingSpaces;\n\n    let inCode = \"\",\n        i = 0;\n    const stack: string[] = [];\n\n    while (i < str.length) {\n        if (str[i] === \"\\\\\" && ++i < str.length) {\n            i++;\n            continue;\n        }\n\n        if (str[i] === \"`\") {\n            const start = i;\n            while (i < str.length && str[i] === \"`\") i++;\n            const ticks = str.slice(start, i);\n            inCode = inCode ? (inCode === ticks ? \"\" : inCode) : ticks;\n            continue;\n        }\n\n        if (inCode) {\n            i++;\n            continue;\n        }\n\n        if (str[i] === \"~\" && str[i + 1] === \"~\") {\n            const prevChar = i > 0 ? str[i - 1] : \" \";\n            const nextChar = i + 2 < str.length ? str[i + 2] : \"\";\n            if (stack[stack.length - 1] === \"~~\" && prevChar !== \" \") stack.pop();\n            else if (nextChar !== \" \") stack.push(\"~~\");\n            i += 2;\n            continue;\n        }\n\n        if (str[i] === \"*\" || str[i] === \"_\") {\n            const char = str[i],\n                start = i;\n            while (i < str.length && str[i] === char) i++;\n\n            const delimiter = str.slice(start, i);\n            const prevChar = start > 0 ? str[start - 1] : \" \";\n            const nextChar = i < str.length ? str[i] : \"\";\n\n            const canOpen = nextChar !== \" \",\n                canClose = prevChar !== \" \",\n                topOfStack = stack[stack.length - 1] || \"\";\n\n            if (canClose && topOfStack.startsWith(char)) {\n                if (topOfStack === delimiter) stack.pop();\n                else if (i === str.length && delimiter.length < topOfStack.length) continue;\n                else if (canOpen) stack.push(delimiter);\n            } else if (canOpen) {\n                stack.push(delimiter);\n            }\n            continue;\n        }\n\n        if (str[i] === \"[\") {\n            if (str[i + 1] !== \" \") stack.push(\"]\");\n            i++;\n            continue;\n        }\n        if (str[i] === \"]\" && stack[stack.length - 1] === \"]\") {\n            stack.pop();\n            if (str[++i] === \"(\") {\n                stack.push(\")\");\n                i++;\n            }\n            continue;\n        }\n        if (str[i] === \")\" && stack[stack.length - 1] === \")\") {\n            stack.pop();\n            i++;\n            continue;\n        }\n\n        i++;\n    }\n\n    return str + (inCode || stack.reverse().join(\"\")) + trailingSpaces;\n}\n"
    }
  ],
  "mcp": {
    "description": "Device-local AI-powered functionality. A ready-to-use chat interface powered by Chrome's native Gemini Nano with streaming responses Relies on react-markdown, remark-gfm, prompt-api-polyfill. Ideal for privacy-first AI interfaces, streaming text responses, and Chrome Gemini Nano integrations.",
    "tags": [
      "native-ai",
      "chat"
    ]
  }
}