diff --git a/bun.lock b/bun.lock index 7ee868a2..02d46605 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@ai-sdk/google": "^2.0.7", "@ai-sdk/groq": "^2.0.6", "@ai-sdk/openai": "^2.0.11", + "@ai-sdk/vercel": "^1.0.11", "@babel/runtime": "^7.28.2", "@clerk/backend": "^2.7.1", "@clerk/clerk-react": "^5.41.0", @@ -93,6 +94,7 @@ "react-router-dom": "^7.8.0", "recharts": "^3.1.2", "rollup": "^4.46.2", + "sanitize-html": "^2.17.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", @@ -147,10 +149,14 @@ "@ai-sdk/openai": ["@ai-sdk/openai@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-t4i+vS825EC0Gc2DdTsC5UkXIu1ScOi363noTD8DuFZp6WFPHRnW6HCyEQKxEm6cNjv3BW89rdXWqq932IFJhA=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-eRD6dZviy31KYz4YvxAR/c6UEYx3p4pCiWZeDdYdAHj0rn8xZlGVxtQRs1qynhz6IYGOo4aLBf9zVW5w0tI/Uw=="], + "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/3Z6lfUp8r+ewFd9yzHkCmPlMOJUXup2Sx3aoUyrdXLhOmAfHRl6Z4lDbIdV0uvw/QYoBcVLJnvXN7ncYeS3uQ=="], + "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.11", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.11", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-ffcMEsExG2dRcNeU/+ClHCdlEmgIJU5x/DVJJoSHqTiACBOHUZuhBc04gWEBZXQMro83DwkXwVH1RcFzr4ZxuA=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -1713,6 +1719,8 @@ "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], @@ -1957,6 +1965,8 @@ "parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="], + "parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -2141,6 +2151,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-html": ["sanitize-html@2.17.0", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2513,6 +2525,10 @@ "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q=="], + + "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q=="], + "@antfu/install-pkg/tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], "@apideck/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -2741,6 +2757,8 @@ "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], + "sanitize-html/htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + "source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], diff --git a/package.json b/package.json index d4cd070f..89791bb2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@ai-sdk/google": "^2.0.7", "@ai-sdk/groq": "^2.0.6", "@ai-sdk/openai": "^2.0.11", + "@ai-sdk/vercel": "^1.0.11", "@babel/runtime": "^7.28.2", "@clerk/backend": "^2.7.1", "@clerk/clerk-react": "^5.41.0", @@ -122,14 +123,14 @@ "react-router-dom": "^7.8.0", "recharts": "^3.1.2", "rollup": "^4.46.2", + "sanitize-html": "^2.17.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "undici": "^7.13.0", "vaul": "^1.1.2", "vercel": "^44.7.3", - "zod": "^4.0.17", - "sanitize-html": "^2.17.0" + "zod": "^4.0.17" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/src/components/EnhancedChatInterface.tsx b/src/components/EnhancedChatInterface.tsx index 8c424491..f1430cd8 100644 --- a/src/components/EnhancedChatInterface.tsx +++ b/src/components/EnhancedChatInterface.tsx @@ -147,8 +147,24 @@ const EnhancedChatInterface: React.FC = () => { // Handle the streaming response if (typeof aiResponse === 'string') { responseContent = aiResponse; - } else if (aiResponse && typeof aiResponse === 'object') { - // Handle streaming response - convert to string + } else if (aiResponse && typeof aiResponse === 'object' && 'textStream' in aiResponse) { + // Properly consume the streaming response + const streamResult = aiResponse as { textStream: AsyncIterable }; + const chunks: string[] = []; + let totalLength = 0; + + for await (const delta of streamResult.textStream) { + const piece = String(delta); + chunks.push(piece); + totalLength += piece.length; + if (totalLength > 50000) { + break; + } + } + + responseContent = chunks.join('').slice(0, 50000); + } else { + // Fallback if response format is unexpected responseContent = 'AI response generated successfully'; } @@ -159,13 +175,13 @@ const EnhancedChatInterface: React.FC = () => { role: 'assistant', metadata: { model: 'ai-assistant', - tokens: undefined, - cost: undefined + tokens: Math.floor(responseContent.length / 4), // Rough estimate + cost: 0.01 // Default cost } }); // Auto-generate chat title if first message - if (messages && 'messages' in messages && messages.messages.length === 0) { + if (messages && typeof messages === 'object' && 'messages' in messages && Array.isArray(messages.messages) && messages.messages.length === 0) { await generateChatTitleFromMessages([ { content: userInput, role: 'user' }, { content: responseContent, role: 'assistant' } @@ -219,8 +235,8 @@ const EnhancedChatInterface: React.FC = () => { // Memoized message list to prevent unnecessary re-renders const memoizedMessages = useMemo(() => { - if (messages && 'messages' in messages) { - return messages.messages || []; + if (messages && typeof messages === 'object' && 'messages' in messages && Array.isArray(messages.messages)) { + return messages.messages; } return []; }, [messages]); @@ -269,7 +285,7 @@ const EnhancedChatInterface: React.FC = () => { { syncPendingEvents, getSubscription: async () => { try { - const base = import.meta.env.VITE_CONVEX_URL as string | undefined; - const url = base - ? `${base.replace(/\/$/, '')}/trpc/billing.getUserSubscription` - : '/trpc/billing.getUserSubscription'; + // Use the correct tRPC endpoint based on vercel.json routing + const url = '/hono/trpc/billing.getUserSubscription'; const token = authTokenManager.getToken(); const res = await fetch(url, { - method: 'GET', + method: 'POST', // tRPC queries use POST headers: { + 'Content-Type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}), - accept: 'application/json', }, - // Include cookies if same-origin; omit if cross-origin bearer-token flow - credentials: base ? 'omit' : 'include', + body: JSON.stringify({}), // Empty body for query + credentials: 'include', }); - if (!res.ok) return null; + if (!res.ok) { + console.error('tRPC subscription fetch failed:', res.status, res.statusText); + return null; + } const json = await res.json(); return json?.result?.data ?? null; - } catch { + } catch (error) { + console.error('Error fetching subscription:', error); return null; } }, diff --git a/src/lib/ai.ts b/src/lib/ai.ts index 153d4036..3f9e0c69 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -26,10 +26,10 @@ const responseCache = new AIResponseCache(); // Cost tracking and limits const MODEL_PRICING = { - 'openai/gpt-oss-120b': { - // Pricing based on Groq docs: $0.15 / 1M input tokens, $0.75 / 1M output tokens - input: 0.15 / 1_000_000, - output: 0.75 / 1_000_000, + 'moonshotai/kimi-k2-instruct': { + // Pricing based on Groq docs: $1.00 / 1M input tokens, $3.00 / 333,333 output tokens + input: 1.00 / 1_000_000, + output: 3.00 / 333_333, } }; @@ -157,13 +157,13 @@ const openrouter = createOpenRouter({ // Get current model instance async function getCurrentModel() { const groq = createGroqInstance(); - return (await groq)('openai/gpt-oss-120b'); + return (await groq)('moonshotai/kimi-k2-instruct'); } // Gemma model (for concise title generation) async function getGemmaModel() { const groq = await createGroqInstance(); - return groq('openai/gpt-oss-120b'); + return groq('moonshotai/kimi-k2-instruct'); } // OpenRouter failsafe model @@ -181,7 +181,7 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: logger.info('Using cached AI response'); aiMonitoring.recordOperation({ operation: 'generateText', - model: 'openai/gpt-oss-120b', + model: 'moonshotai/kimi-k2-instruct', duration: 0, success: true, inputTokens: 0, @@ -216,7 +216,7 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: const estimatedInputTokens = Math.ceil(prompt.length / 4); const estimatedOutputTokens = 8000; - const estimatedCost = calculateCost('openai/gpt-oss-120b', estimatedInputTokens, estimatedOutputTokens); + const estimatedCost = calculateCost('moonshotai/kimi-k2-instruct', estimatedInputTokens, estimatedOutputTokens); checkCostLimit(estimatedCost); @@ -227,7 +227,7 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: }); const currentModel = await getCurrentModel(); - span.setAttribute("model", "openai/gpt-oss-120b"); + span.setAttribute("model", "moonshotai/kimi-k2-instruct"); const { text, usage } = await circuitBreaker.execute( () => withRetry( @@ -235,21 +235,21 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: () => withTimeout(generateText({ model: currentModel, prompt, - temperature: 0.7, + temperature: 0.6, }), 60_000), 'generateText', - { model: 'openai/gpt-oss-120b', promptLength: prompt.length } + { model: 'moonshotai/kimi-k2-instruct', promptLength: prompt.length } ), 'AI Text Generation' ), 'generateAIResponse' ) - const actualCost = usage ? calculateCost('openai/gpt-oss-120b', usage.inputTokens || 0, usage.outputTokens || 0) : estimatedCost; + const actualCost = usage ? calculateCost('moonshotai/kimi-k2-instruct', usage.inputTokens || 0, usage.outputTokens || 0) : estimatedCost; addTodayCost(actualCost); await recordAIConversation({ - model: 'openai/gpt-oss-120b', + model: 'moonshotai/kimi-k2-instruct', inputTokens: usage?.inputTokens || 0, outputTokens: usage?.outputTokens || 0, cost: actualCost, @@ -262,7 +262,7 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: logger.info("AI text generation completed", { responseLength: text.length, - model: "openai/gpt-oss-120b", + model: "moonshotai/kimi-k2-instruct", actualCost: actualCost.toFixed(6), inputTokens: usage?.inputTokens || 0, outputTokens: usage?.outputTokens || 0, @@ -271,7 +271,7 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: aiMonitoring.recordOperation({ operation: 'generateText', - model: 'openai/gpt-oss-120b', + model: 'moonshotai/kimi-k2-instruct', duration: Date.now() - startTime, success: true, inputTokens: usage?.inputTokens || 0, @@ -297,7 +297,7 @@ export async function generateAIResponse(prompt: string, options?: { skipCache?: aiMonitoring.recordOperation({ operation: 'generateText', - model: 'openai/gpt-oss-120b', + model: 'moonshotai/kimi-k2-instruct', duration: Date.now() - startTime, success: false, error: aiError.message, @@ -366,7 +366,7 @@ export async function streamAIResponse(prompt: string) { const fullPrompt = systemPrompt + "\n\n" + prompt; const estimatedInputTokens = Math.ceil(fullPrompt.length / 4); const estimatedOutputTokens = 8000; - const estimatedCost = calculateCost('openai/gpt-oss-120b', estimatedInputTokens, estimatedOutputTokens); + const estimatedCost = calculateCost('moonshotai/kimi-k2-instruct', estimatedInputTokens, estimatedOutputTokens); checkCostLimit(estimatedCost); @@ -377,7 +377,7 @@ export async function streamAIResponse(prompt: string) { }); const model = await getCurrentModel(); - span.setAttribute("model", "openai/gpt-oss-120b"); + span.setAttribute("model", "moonshotai/kimi-k2-instruct"); const result = await circuitBreaker.execute( () => withRetry( @@ -388,10 +388,10 @@ export async function streamAIResponse(prompt: string) { { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], - temperature: 0.7, + temperature: 0.6, }), 'streamText', - { model: 'openai/gpt-oss-120b', promptLength: prompt.length } + { model: 'moonshotai/kimi-k2-instruct', promptLength: prompt.length } ), 'AI Stream Generation' ), @@ -401,21 +401,21 @@ export async function streamAIResponse(prompt: string) { addTodayCost(estimatedCost); await recordAIConversation({ - model: 'openai/gpt-oss-120b', + model: 'moonshotai/kimi-k2-instruct', inputTokens: estimatedInputTokens, outputTokens: estimatedOutputTokens, cost: estimatedCost, }); logger.info("AI streaming started successfully", { - model: "openai/gpt-oss-120b", + model: "moonshotai/kimi-k2-instruct", estimatedCost: estimatedCost.toFixed(6), dailyCost: getTodayCost().toFixed(4) }); aiMonitoring.recordOperation({ operation: 'streamText', - model: 'openai/gpt-oss-120b', + model: 'moonshotai/kimi-k2-instruct', duration: Date.now() - startTime, success: true, inputTokens: estimatedInputTokens,