diff --git a/bun.lock b/bun.lock index fa5118f8..6d4207d8 100644 --- a/bun.lock +++ b/bun.lock @@ -39,7 +39,6 @@ "@upstash/redis": "^1.35.0", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", - "QCX": ".", "ai": "^4.3.19", "build": "^0.1.4", "class-variance-authority": "^0.7.1", @@ -1032,8 +1031,6 @@ "@vercel/speed-insights": ["@vercel/speed-insights@1.2.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw=="], - "QCX": ["QCX@file:", { "dependencies": { "@ai-sdk/amazon-bedrock": "^1.1.6", "@ai-sdk/anthropic": "^1.2.12", "@ai-sdk/google": "^1.2.22", "@ai-sdk/openai": "^1.3.24", "@ai-sdk/xai": "^1.2.18", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.0.1", "@mapbox/mapbox-gl-draw": "^1.5.0", "@modelcontextprotocol/sdk": "^1.13.0", "@radix-ui/react-alert-dialog": "^1.1.10", "@radix-ui/react-avatar": "^1.1.6", "@radix-ui/react-checkbox": "^1.2.2", "@radix-ui/react-collapsible": "^1.1.7", "@radix-ui/react-dialog": "^1.1.10", "@radix-ui/react-dropdown-menu": "^2.1.11", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-radio-group": "^1.3.4", "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slider": "^1.3.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-switch": "^1.2.2", "@radix-ui/react-tabs": "^1.1.9", "@radix-ui/react-toast": "^1.2.11", "@radix-ui/react-tooltip": "^1.2.3", "@smithery/cli": "^1.2.5", "@smithery/sdk": "^1.0.4", "@supabase/ssr": "^0.3.0", "@supabase/supabase-js": "^2.0.0", "@tailwindcss/typography": "^0.5.16", "@turf/turf": "^7.2.0", "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", "QCX": ".", "ai": "^4.3.19", "build": "^0.1.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cookie": "^0.6.0", "dotenv": "^16.5.0", "drizzle-kit": "^0.31.1", "drizzle-orm": "^0.29.0", "embla-carousel-react": "^8.6.0", "exa-js": "^1.6.13", "framer-motion": "^12.15.0", "katex": "^0.16.22", "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", "next": "^15.3.3", "next-themes": "^0.3.0", "open-codex": "^0.1.30", "pg": "^8.16.2", "radix-ui": "^1.3.4", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.56.2", "react-icons": "^5.5.0", "react-markdown": "^9.1.0", "react-textarea-autosize": "^8.5.9", "react-toastify": "^10.0.6", "rehype-external-links": "^3.0.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "smithery": "^0.5.2", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "use-mcp": "^0.0.9", "uuid": "^9.0.0", "zod": "^3.23.8" }, "devDependencies": { "@types/cookie": "^0.6.0", "@types/mapbox-gl": "^3.4.1", "@types/node": "^20.17.30", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/uuid": "^9.0.0", "cross-env": "^7.0.3", "eslint": "^8.57.1", "eslint-config-next": "^14.2.28", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.3" } }], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -2568,8 +2565,6 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - "QCX/QCX": ["QCX@file:.", {}], - "ai/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index b0bf2166..a9b8c044 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -69,42 +69,45 @@ export const ChatPanel = forwardRef(({ messages, i } } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!input && !selectedFile) { - return + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() && !selectedFile) { + return; } - const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = [] + // Create the user message content first, while we still have the input and file + const content: ({ type: 'text'; text: string } | { type: 'image'; image: File })[] = []; if (input) { - content.push({ type: 'text', text: input }) + content.push({ type: 'text', text: input }); } if (selectedFile && selectedFile.type.startsWith('image/')) { - content.push({ - type: 'image', - image: URL.createObjectURL(selectedFile) - }) + content.push({ type: 'image', image: selectedFile }); } - setMessages(currentMessages => [ - ...currentMessages, - { - id: nanoid(), - component: - } - ]) - - const formData = new FormData(e.currentTarget) + // Prepare the form data for the server action + const formData = new FormData(e.currentTarget); if (selectedFile) { - formData.append('file', selectedFile) + formData.append('file', selectedFile); } - setInput('') - clearAttachment() + // Clear the input fields for the user + setInput(''); + clearAttachment(); - const responseMessage = await submit(formData) - setMessages(currentMessages => [...currentMessages, responseMessage as any]) - } + // Call the server action. It will immediately return a streamable UI component. + const responseMessage = submit(formData); + + // Update the UI state with both the user's message and the initial assistant response + // in a single operation. This ensures the component tree structure is consistent. + setMessages(currentMessages => [ + ...currentMessages, + { + id: nanoid(), + component: , + }, + responseMessage as any, + ]); + }; const handleClear = async () => { setMessages([]) diff --git a/components/user-message.tsx b/components/user-message.tsx index 03b8ea8d..8b9690dd 100644 --- a/components/user-message.tsx +++ b/components/user-message.tsx @@ -1,10 +1,10 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import Image from 'next/image' import { ChatShare } from './chat-share' type UserMessageContentPart = | { type: 'text'; text: string } - | { type: 'image'; image: string } // data URL + | { type: 'image'; image: string | File } // Can be a data URL or a File object type UserMessageProps = { content: string | UserMessageContentPart[] @@ -18,6 +18,7 @@ export const UserMessage: React.FC = ({ showShare = false }) => { const enableShare = process.env.ENABLE_SHARE === 'true' + const [imageUrl, setImageUrl] = useState(null) // Normalize content to an array const contentArray = @@ -27,21 +28,48 @@ export const UserMessage: React.FC = ({ const textPart = contentArray.find( (part): part is { type: 'text'; text: string } => part.type === 'text' )?.text - const imagePart = contentArray.find( - (part): part is { type: 'image'; image: string } => part.type === 'image' + const imageContent = contentArray.find( + (part): part is { type: 'image'; image: string | File } => + part.type === 'image' )?.image + useEffect(() => { + let objectUrl: string | null = null + + if (imageContent instanceof File) { + objectUrl = URL.createObjectURL(imageContent) + setImageUrl(objectUrl) + } else if (typeof imageContent === 'string') { + setImageUrl(imageContent) + } + + // Cleanup function to revoke the object URL + return () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl) + setImageUrl(null) // Reset state on cleanup + } + } + }, [imageContent]) + return (
- {imagePart && ( + {imageUrl && (
attachment { + // Optional: Revoke URL after image has loaded to free memory sooner + // if the image is cached by the browser. + // Note: This might cause issues if the component re-renders and + // the browser needs to fetch the image again. + // URL.revokeObjectURL(imageUrl); + }} />
)} diff --git a/dev.log b/dev.log new file mode 100644 index 00000000..e69de29b diff --git a/mapbox_mcp/hooks.ts b/mapbox_mcp/hooks.ts index 326056db..b1e43147 100644 --- a/mapbox_mcp/hooks.ts +++ b/mapbox_mcp/hooks.ts @@ -8,7 +8,7 @@ type Tool = { name: string; // Add other properties as needed based on your usage }; -import { getModel } from 'QCX/lib/utils'; +import { getModel } from '../lib/utils'; // Types for location and mapping data interface LocationResult {