From 39bb9e3dcfc2727b7c9fb580d272e58b63025970 Mon Sep 17 00:00:00 2001 From: Kenny <966806+kennyu@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:12:39 -0600 Subject: [PATCH 1/7] feat(ai): add Vercel AI SDK dependencies and provider config --- chartsmith-app/lib/ai/provider.ts | 11 ++ chartsmith-app/package-lock.json | 181 ++++++++++++++++++++++++++++++ chartsmith-app/package.json | 3 + 3 files changed, 195 insertions(+) create mode 100644 chartsmith-app/lib/ai/provider.ts diff --git a/chartsmith-app/lib/ai/provider.ts b/chartsmith-app/lib/ai/provider.ts new file mode 100644 index 00000000..926c4a29 --- /dev/null +++ b/chartsmith-app/lib/ai/provider.ts @@ -0,0 +1,11 @@ +import { createAnthropic } from '@ai-sdk/anthropic'; + +export const anthropic = createAnthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// Default model for chat +export const chatModel = anthropic('claude-sonnet-4-20250514'); + +// Model for intent classification (faster/cheaper) +export const intentModel = anthropic('claude-3-5-sonnet-20241022'); diff --git a/chartsmith-app/package-lock.json b/chartsmith-app/package-lock.json index 17c24db6..cd6bf471 100644 --- a/chartsmith-app/package-lock.json +++ b/chartsmith-app/package-lock.json @@ -8,11 +8,14 @@ "name": "chartsmith-app", "version": "0.1.0", "dependencies": { + "@ai-sdk/anthropic": "^2.0.53", + "@ai-sdk/react": "^2.0.109", "@anthropic-ai/sdk": "^0.39.0", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-toast": "^1.2.7", "@tailwindcss/typography": "^0.5.16", "@types/diff": "^7.0.1", + "ai": "^5.0.108", "autoprefixer": "^10.4.20", "centrifuge": "^5.3.4", "class-variance-authority": "^0.7.1", @@ -69,6 +72,92 @@ "typescript": "^5.8.2" } }, + "node_modules/@ai-sdk/anthropic": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.53.tgz", + "integrity": "sha512-ih7NV+OFSNWZCF+tYYD7ovvvM+gv7TRKQblpVohg2ipIwC9Y0TirzocJVREzZa/v9luxUwFbsPji++DUDWWxsg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz", + "integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", + "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.109", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.109.tgz", + "integrity": "sha512-5qM8KuN7bv7E+g6BXkSAYLFjwIfMSTKOA1prjg1zEShJXJyLSc+Yqkd3EfGibm75b7nJAqJNShurDmR/IlQqFQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.18", + "ai": "5.0.108", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -2214,6 +2303,15 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3058,6 +3156,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3626,6 +3730,15 @@ "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", "license": "ISC" }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -3692,6 +3805,24 @@ "node": ">= 8.0.0" } }, + "node_modules/ai": { + "version": "5.0.108", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.108.tgz", + "integrity": "sha512-Jex3Lb7V41NNpuqJHKgrwoU6BCLHdI1Pg4qb4GJH4jRIDRXUBySJErHjyN4oTCwbiYCeb/8II9EnqSRPq9EifA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.18", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5962,6 +6093,15 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -8357,6 +8497,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -10472,6 +10618,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -12261,6 +12408,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.7.tgz", + "integrity": "sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", @@ -12408,6 +12568,18 @@ "node": ">=0.8" } }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -13000,6 +13172,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/chartsmith-app/package.json b/chartsmith-app/package.json index 88894b62..19937326 100644 --- a/chartsmith-app/package.json +++ b/chartsmith-app/package.json @@ -18,11 +18,14 @@ "test:parseDiff": "jest parseDiff" }, "dependencies": { + "@ai-sdk/anthropic": "^2.0.53", + "@ai-sdk/react": "^2.0.109", "@anthropic-ai/sdk": "^0.39.0", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-toast": "^1.2.7", "@tailwindcss/typography": "^0.5.16", "@types/diff": "^7.0.1", + "ai": "^5.0.108", "autoprefixer": "^10.4.20", "centrifuge": "^5.3.4", "class-variance-authority": "^0.7.1", From 14db289661cd5720b90a5014f1adc3a2486df1cf Mon Sep 17 00:00:00 2001 From: Kenny <966806+kennyu@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:18:11 -0600 Subject: [PATCH 2/7] feat(ai): migrate intent classification to AI SDK --- chartsmith-app/lib/llm/prompt-type.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/chartsmith-app/lib/llm/prompt-type.ts b/chartsmith-app/lib/llm/prompt-type.ts index b56ea031..6f01a935 100644 --- a/chartsmith-app/lib/llm/prompt-type.ts +++ b/chartsmith-app/lib/llm/prompt-type.ts @@ -1,4 +1,5 @@ -import Anthropic from '@anthropic-ai/sdk'; +import { generateText } from 'ai'; +import { intentModel } from '@/lib/ai/provider'; import { logger } from "@/lib/utils/logger"; export enum PromptType { @@ -18,25 +19,18 @@ export interface PromptIntent { export async function promptType(message: string): Promise { try { - const anthropic = new Anthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, - }); - - const msg = await anthropic.messages.create({ - model: "claude-3-5-sonnet-20241022", - max_tokens: 1024, - system: `You are ChartSmith, an expert at creating Helm charts for Kuberentes. + const { text } = await generateText({ + model: intentModel, + system: `You are ChartSmith, an expert at creating Helm charts for Kubernetes. You are invited to participate in an existing conversation between a user and an expert. The expert just provided a recommendation on how to plan the Helm chart to the user. The user is about to ask a question. You should decide if the user is asking for a change to the plan/chart, or if they are just asking a conversational question. -Be exceptionally brief and precise. in your response. -Only say "plan" or "chat" in your response. -`, - messages: [ - { role: "user", content: message } - ]}); - const text = msg.content[0].type === 'text' ? msg.content[0].text : ''; +Be exceptionally brief and precise in your response. +Only say "plan" or "chat" in your response.`, + prompt: message, + maxOutputTokens: 1024, + }); if (text.toLowerCase().includes("plan")) { return PromptType.Plan; From ac68645dfe10790e9d85e6ae43cfbefb327bb7af Mon Sep 17 00:00:00 2001 From: Kenny <966806+kennyu@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:22:12 -0600 Subject: [PATCH 3/7] feat(ai): add streaming chat API route with tools --- chartsmith-app/app/api/chat/route.ts | 61 ++++++++++++++++++ chartsmith-app/lib/ai/context.ts | 95 ++++++++++++++++++++++++++++ chartsmith-app/package.json | 1 + 3 files changed, 157 insertions(+) create mode 100644 chartsmith-app/app/api/chat/route.ts create mode 100644 chartsmith-app/lib/ai/context.ts diff --git a/chartsmith-app/app/api/chat/route.ts b/chartsmith-app/app/api/chat/route.ts new file mode 100644 index 00000000..dfd90941 --- /dev/null +++ b/chartsmith-app/app/api/chat/route.ts @@ -0,0 +1,61 @@ +import { streamText, tool, convertToModelMessages, UIMessage } from 'ai'; +import { chatModel } from '@/lib/ai/provider'; +import { z } from 'zod'; +import { getWorkspaceContext } from '@/lib/ai/context'; + +export const maxDuration = 60; + +export async function POST(req: Request) { + const { messages, workspaceId, chartId }: { + messages: UIMessage[]; + workspaceId: string; + chartId?: string; + } = await req.json(); + + // Get workspace context (chart structure, relevant files, etc.) + const context = await getWorkspaceContext(workspaceId, chartId, messages); + + const result = streamText({ + model: chatModel, + system: context.systemPrompt, + messages: convertToModelMessages(messages), + tools: { + latest_subchart_version: tool({ + description: 'Return the latest version of a subchart from name', + inputSchema: z.object({ + chart_name: z.string().describe('The subchart name to get the latest version of'), + }), + execute: async ({ chart_name }) => { + // Call the existing recommendation service + try { + const response = await fetch( + `${process.env.INTERNAL_API_URL}/api/recommendations/subchart/${encodeURIComponent(chart_name)}` + ); + if (!response.ok) return '?'; + const data = await response.json(); + return data.version || '?'; + } catch { + return '?'; + } + }, + }), + latest_kubernetes_version: tool({ + description: 'Return the latest version of Kubernetes', + inputSchema: z.object({ + semver_field: z.enum(['major', 'minor', 'patch']).describe('One of major, minor, or patch'), + }), + execute: async ({ semver_field }) => { + switch (semver_field) { + case 'major': return '1'; + case 'minor': return '1.32'; + case 'patch': return '1.32.1'; + default: return '1.32.1'; + } + }, + }), + }, + maxOutputTokens: 8192, + }); + + return result.toUIMessageStreamResponse(); +} diff --git a/chartsmith-app/lib/ai/context.ts b/chartsmith-app/lib/ai/context.ts new file mode 100644 index 00000000..94f2af2f --- /dev/null +++ b/chartsmith-app/lib/ai/context.ts @@ -0,0 +1,95 @@ +import { getWorkspace, listPlans } from '@/lib/workspace/workspace'; +import { listMessagesForWorkspace } from '@/lib/workspace/chat'; +import { UIMessage } from 'ai'; + +const CHAT_SYSTEM_PROMPT = `You are ChartSmith, an AI assistant specialized in creating and managing Helm charts for Kubernetes. +You help developers and operators understand, modify, and improve their Helm charts. +Be helpful, concise, and technical when appropriate.`; + +const CHAT_INSTRUCTIONS = `When answering questions: +1. Consider the chart structure and existing files +2. Reference specific files when relevant +3. Provide code examples when helpful +4. Be aware of Helm best practices`; + +export interface WorkspaceContext { + systemPrompt: string; + chartStructure: string; + relevantFiles: Array<{ path: string; content: string }>; +} + +export async function getWorkspaceContext( + workspaceId: string, + chartId?: string, + messages?: UIMessage[] +): Promise { + const workspace = await getWorkspace(workspaceId); + if (!workspace) { + throw new Error(`Workspace not found: ${workspaceId}`); + } + + // Get chart structure + const chart = chartId + ? workspace.charts.find(c => c.id === chartId) + : workspace.charts[0]; + + const chartStructure = chart + ? chart.files.map(f => `File: ${f.filePath}`).join('\n') + : ''; + + // Get relevant files from the chart (limit to 10) + const relevantFiles: Array<{ path: string; content: string }> = []; + if (chart) { + for (const file of chart.files.slice(0, 10)) { + relevantFiles.push({ + path: file.filePath, + content: file.content, + }); + } + } + + // Build system prompt with context + let systemPrompt = CHAT_SYSTEM_PROMPT + '\n\n' + CHAT_INSTRUCTIONS; + + if (chartStructure) { + systemPrompt += `\n\nCurrent chart structure:\n${chartStructure}`; + } + + // Add relevant file contents + for (const file of relevantFiles) { + systemPrompt += `\n\nFile: ${file.path}\n\`\`\`\n${file.content}\n\`\`\``; + } + + // Get previous plan and chat history if available + try { + const plans = await listPlans(workspaceId); + const plan = plans.length > 0 ? plans[0] : null; // Most recent plan (ordered by created_at DESC) + + if (plan) { + systemPrompt += `\n\nMost recent plan:\n${plan.description || '(No description)'}`; + + // Get chat messages and filter to those after the plan + const allMessages = await listMessagesForWorkspace(workspaceId); + const planCreatedAt = new Date(plan.createdAt); + const previousChats = allMessages.filter(msg => + new Date(msg.createdAt) > planCreatedAt + ); + + if (previousChats.length > 0) { + systemPrompt += '\n\nPrevious conversation context:'; + for (const chat of previousChats.slice(-5)) { + if (chat.prompt) systemPrompt += `\nUser: ${chat.prompt}`; + if (chat.response) systemPrompt += `\nAssistant: ${chat.response}`; + } + } + } + } catch { + // No plan exists or error fetching, continue without it + } + + return { + systemPrompt, + chartStructure, + relevantFiles, + }; +} diff --git a/chartsmith-app/package.json b/chartsmith-app/package.json index 19937326..9d4d5546 100644 --- a/chartsmith-app/package.json +++ b/chartsmith-app/package.json @@ -26,6 +26,7 @@ "@tailwindcss/typography": "^0.5.16", "@types/diff": "^7.0.1", "ai": "^5.0.108", + "zod": "^3.24.1", "autoprefixer": "^10.4.20", "centrifuge": "^5.3.4", "class-variance-authority": "^0.7.1", From d9bfd746671f31a423799c971b51279f4c66478b Mon Sep 17 00:00:00 2001 From: Kenny <966806+kennyu@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:22:35 -0600 Subject: [PATCH 4/7] feat(ai): migrate ChatContainer to useChat hook --- .../components/AIStreamingMessage.tsx | 79 +++++++++++++++ chartsmith-app/components/ChatContainer.tsx | 83 ++++++++++++---- chartsmith-app/hooks/useAIChat.ts | 96 +++++++++++++++++++ 3 files changed, 242 insertions(+), 16 deletions(-) create mode 100644 chartsmith-app/components/AIStreamingMessage.tsx create mode 100644 chartsmith-app/hooks/useAIChat.ts diff --git a/chartsmith-app/components/AIStreamingMessage.tsx b/chartsmith-app/components/AIStreamingMessage.tsx new file mode 100644 index 00000000..62b4515e --- /dev/null +++ b/chartsmith-app/components/AIStreamingMessage.tsx @@ -0,0 +1,79 @@ +'use client'; + +import React from 'react'; +import Image from 'next/image'; +import ReactMarkdown from 'react-markdown'; +import { UIMessage } from 'ai'; +import { useTheme } from '../contexts/ThemeContext'; +import { Session } from '@/lib/types/session'; +import { Loader2 } from 'lucide-react'; + +interface AIStreamingMessageProps { + message: UIMessage; + session: Session; + isStreaming?: boolean; +} + +export function AIStreamingMessage({ message, session, isStreaming }: AIStreamingMessageProps) { + const { theme } = useTheme(); + + // Extract text content from parts + const textContent = message.parts + ?.filter((part): part is { type: 'text'; text: string } => part.type === 'text') + .map(part => part.text) + .join('') || ''; + + if (message.role === 'user') { + return ( +
+
+
+ {session.user.name} +
+
+ {textContent} +
+
+
+
+
+ ); + } + + if (message.role === 'assistant') { + return ( +
+
+
+
+ ChartSmith + {isStreaming && ( + + )} +
+
+
+ {textContent ? ( + {textContent} + ) : isStreaming ? ( +
+
+
+ generating response... +
+
+ ) : null} +
+
+
+ ); + } + + return null; +} diff --git a/chartsmith-app/components/ChatContainer.tsx b/chartsmith-app/components/ChatContainer.tsx index 5761674a..72a1add8 100644 --- a/chartsmith-app/components/ChatContainer.tsx +++ b/chartsmith-app/components/ChatContainer.tsx @@ -1,15 +1,17 @@ "use client"; import React, { useState, useRef, useEffect } from "react"; -import { Send, Loader2, Users, Code, User, Sparkles } from "lucide-react"; +import { Send, Loader2, Code, User, Sparkles, StopCircle } from "lucide-react"; import { useTheme } from "../contexts/ThemeContext"; import { Session } from "@/lib/types/session"; import { ChatMessage } from "./ChatMessage"; +import { AIStreamingMessage } from "./AIStreamingMessage"; import { messagesAtom, workspaceAtom, isRenderingAtom } from "@/atoms/workspace"; import { useAtom } from "jotai"; import { createChatMessageAction } from "@/lib/workspace/actions/create-chat-message"; import { ScrollingContent } from "./ScrollingContent"; import { NewChartChatMessage } from "./NewChartChatMessage"; import { NewChartContent } from "./NewChartContent"; +import { useAIChat } from "@/hooks/useAIChat"; interface ChatContainerProps { session: Session; @@ -23,8 +25,22 @@ export function ChatContainer({ session }: ChatContainerProps) { const [chatInput, setChatInput] = useState(""); const [selectedRole, setSelectedRole] = useState<"auto" | "developer" | "operator">("auto"); const [isRoleMenuOpen, setIsRoleMenuOpen] = useState(false); + const [useAIStreaming, setUseAIStreaming] = useState(false); // Toggle for AI SDK streaming const roleMenuRef = useRef(null); - + + // AI SDK chat hook + const { + messages: aiMessages, + input: aiInput, + setInput: setAiInput, + handleSubmit: handleAiSubmit, + isLoading: aiIsLoading, + stop: aiStop, + } = useAIChat({ + session, + workspaceId: workspace?.id || '', + }); + // No need for refs as ScrollingContent manages its own scrolling // Close the role menu when clicking outside @@ -34,7 +50,7 @@ export function ChatContainer({ session }: ChatContainerProps) { setIsRoleMenuOpen(false); } }; - + document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); @@ -45,16 +61,26 @@ export function ChatContainer({ session }: ChatContainerProps) { return null; } + // Use AI SDK or legacy based on toggle + const currentInput = useAIStreaming ? aiInput : chatInput; + const setCurrentInput = useAIStreaming ? setAiInput : setChatInput; + const currentIsLoading = useAIStreaming ? aiIsLoading : isRendering; + const handleSubmitChat = async (e: React.FormEvent) => { e.preventDefault(); - if (!chatInput.trim() || isRendering) return; // Don't submit if rendering is in progress - - if (!session || !workspace) return; - const chatMessage = await createChatMessageAction(session, workspace.id, chatInput.trim(), selectedRole); - setMessages(prev => [...prev, chatMessage]); + if (useAIStreaming) { + // Use AI SDK streaming + handleAiSubmit(e, selectedRole); + } else { + // Legacy: Use existing implementation + if (!currentInput.trim() || isRendering) return; + if (!session || !workspace) return; - setChatInput(""); + const chatMessage = await createChatMessageAction(session, workspace.id, currentInput.trim(), selectedRole); + setMessages(prev => [...prev, chatMessage]); + setChatInput(""); + } }; const getRoleLabel = (role: "auto" | "developer" | "operator"): string => { @@ -98,7 +124,8 @@ export function ChatContainer({ session }: ChatContainerProps) {
- {messages.map((item, index) => ( + {/* Existing messages from database */} + {messages.map((item) => (
))} + {/* AI SDK streaming messages */} + {useAIStreaming && aiMessages.map((message) => ( + + ))}