From 364c32cfce5cd61f3536948aceadbf8edfa10dc2 Mon Sep 17 00:00:00 2001 From: alexalbertt Date: Mon, 2 Sep 2024 23:34:15 -0700 Subject: [PATCH 1/6] customer support agent --- LICENSE | 21 + customer-support-agent/.eslintrc.json | 3 + customer-support-agent/.gitignore | 36 + customer-support-agent/README.md | 267 + customer-support-agent/amplify.yml | 21 + customer-support-agent/app/api/chat/route.ts | 302 + customer-support-agent/app/favicon.ico | Bin 0 -> 25931 bytes customer-support-agent/app/globals.css | 69 + customer-support-agent/app/layout.tsx | 33 + .../app/lib/customer_support_categories.json | 92 + customer-support-agent/app/lib/utils.ts | 95 + customer-support-agent/app/page.tsx | 25 + customer-support-agent/components.json | 17 + .../components/ChatArea.tsx | 725 ++ .../components/FullSourceModal.tsx | 48 + .../components/LeftSidebar.tsx | 193 + .../components/RightSidebar.tsx | 210 + .../components/TopNavBar.tsx | 155 + .../components/theme-provider.tsx | 9 + .../components/ui/avatar.tsx | 50 + .../components/ui/button.tsx | 56 + customer-support-agent/components/ui/card.tsx | 86 + .../components/ui/dialog.tsx | 122 + .../components/ui/dropdown-menu.tsx | 200 + .../components/ui/input.tsx | 25 + .../components/ui/textarea.tsx | 24 + customer-support-agent/config.ts | 11 + customer-support-agent/lib/utils.ts | 6 + customer-support-agent/next.config.mjs | 17 + customer-support-agent/package-lock.json | 9946 +++++++++++++++++ customer-support-agent/package.json | 54 + customer-support-agent/postcss.config.mjs | 8 + customer-support-agent/public/ant-logo.svg | 12 + customer-support-agent/public/claude-icon.svg | 9 + customer-support-agent/public/next.svg | 1 + customer-support-agent/public/vercel.svg | 1 + .../public/wordmark-dark.svg | 11 + customer-support-agent/public/wordmark.svg | 11 + customer-support-agent/styles/themes.js | 392 + customer-support-agent/tailwind.config.ts | 97 + customer-support-agent/tsconfig.json | 26 + .../tutorial/access-keys.png | Bin 0 -> 61707 bytes customer-support-agent/tutorial/attach.png | Bin 0 -> 164538 bytes customer-support-agent/tutorial/bedrock.png | Bin 0 -> 27193 bytes .../tutorial/choose-source.png | Bin 0 -> 223324 bytes .../tutorial/create-knowledge-base.png | Bin 0 -> 84900 bytes .../tutorial/create-user.png | Bin 0 -> 60124 bytes customer-support-agent/tutorial/preview.png | Bin 0 -> 964524 bytes 48 files changed, 13486 insertions(+) create mode 100644 LICENSE create mode 100644 customer-support-agent/.eslintrc.json create mode 100644 customer-support-agent/.gitignore create mode 100644 customer-support-agent/README.md create mode 100644 customer-support-agent/amplify.yml create mode 100644 customer-support-agent/app/api/chat/route.ts create mode 100644 customer-support-agent/app/favicon.ico create mode 100644 customer-support-agent/app/globals.css create mode 100644 customer-support-agent/app/layout.tsx create mode 100644 customer-support-agent/app/lib/customer_support_categories.json create mode 100644 customer-support-agent/app/lib/utils.ts create mode 100644 customer-support-agent/app/page.tsx create mode 100644 customer-support-agent/components.json create mode 100644 customer-support-agent/components/ChatArea.tsx create mode 100644 customer-support-agent/components/FullSourceModal.tsx create mode 100644 customer-support-agent/components/LeftSidebar.tsx create mode 100644 customer-support-agent/components/RightSidebar.tsx create mode 100644 customer-support-agent/components/TopNavBar.tsx create mode 100644 customer-support-agent/components/theme-provider.tsx create mode 100644 customer-support-agent/components/ui/avatar.tsx create mode 100644 customer-support-agent/components/ui/button.tsx create mode 100644 customer-support-agent/components/ui/card.tsx create mode 100644 customer-support-agent/components/ui/dialog.tsx create mode 100644 customer-support-agent/components/ui/dropdown-menu.tsx create mode 100644 customer-support-agent/components/ui/input.tsx create mode 100644 customer-support-agent/components/ui/textarea.tsx create mode 100644 customer-support-agent/config.ts create mode 100644 customer-support-agent/lib/utils.ts create mode 100644 customer-support-agent/next.config.mjs create mode 100644 customer-support-agent/package-lock.json create mode 100644 customer-support-agent/package.json create mode 100644 customer-support-agent/postcss.config.mjs create mode 100644 customer-support-agent/public/ant-logo.svg create mode 100644 customer-support-agent/public/claude-icon.svg create mode 100644 customer-support-agent/public/next.svg create mode 100644 customer-support-agent/public/vercel.svg create mode 100644 customer-support-agent/public/wordmark-dark.svg create mode 100644 customer-support-agent/public/wordmark.svg create mode 100644 customer-support-agent/styles/themes.js create mode 100644 customer-support-agent/tailwind.config.ts create mode 100644 customer-support-agent/tsconfig.json create mode 100644 customer-support-agent/tutorial/access-keys.png create mode 100644 customer-support-agent/tutorial/attach.png create mode 100644 customer-support-agent/tutorial/bedrock.png create mode 100644 customer-support-agent/tutorial/choose-source.png create mode 100644 customer-support-agent/tutorial/create-knowledge-base.png create mode 100644 customer-support-agent/tutorial/create-user.png create mode 100644 customer-support-agent/tutorial/preview.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e0d914d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Anthropic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/customer-support-agent/.eslintrc.json b/customer-support-agent/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/customer-support-agent/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/customer-support-agent/.gitignore b/customer-support-agent/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/customer-support-agent/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/customer-support-agent/README.md b/customer-support-agent/README.md new file mode 100644 index 00000000..6b3f3bdf --- /dev/null +++ b/customer-support-agent/README.md @@ -0,0 +1,267 @@ +# Claude Customer Support Agent + +An advanced, fully customizable customer support chat interface powered by Claude and leveraging Amazon Bedrock Knowledge Bases for knowledge retrieval. +![preview](tutorial/preview.png) + +## Key Features + +- AI-powered chat using Anthropic's Claude model +- Amazong Bedrock integration for contextual knowledge retrieval +- Real-time thinking & debug information display +- Knowledge base source visualization +- User mood detection & appropriate agent redirection +- Highly customizable UI with shadcn/ui components + +## Getting Started + +1. Clone this repository +2. Install dependencies: `npm install` +3. Set up your environment variables (see Configuration section) +4. Run the development server: `npm run dev` +5. Open [http://localhost:3000](http://localhost:3000) in your browser + +## ⚙️ Configuration + +Create a `.env.local` file in the root directory with the following variables: + +``` +ANTHROPIC_API_KEY=your_anthropic_api_key +BAWS_ACCESS_KEY_ID=your_aws_access_key +BAWS_SECRET_ACCESS_KEY=your_aws_secret_key +``` + +Note: We are adding a 'B' in front of the AWS environment variables for a reason that will be discussed later the the deployment section. + +## How to Get Your Keys + +### Anthropic API Key + +1. Visit [console.anthropic.com](https://console.anthropic.com/dashboard) +2. Sign up or log in to your account +3. Click on "Get API keys" +4. Copy the key and paste it into your `.env.local` file + +### AWS Access Key and Secret Key + +Follow these steps to obtain your AWS credentials: + +1. Log in to the AWS Management Console +2. Navigate to the IAM (Identity and Access Management) dashboard + +3. In the left sidebar, click on "Users" + +4. Click "Create user" and follow the prompts to create a new user + ![Add User](tutorial/create-user.png) +5. On the Set Permission page, select the "Attach policies directly" policy + ![Attach Policy](tutorial/aws-attach-policy.png) +5. On the permissions page, use the "AmazonBedrockFullAccess" policy + ![Attach Policy](tutorial/bedrock.png) +6. Review and create the user +7. On the Summary page, click on Create access key. +8. Then select "Application running on an AWS compute service". Add a description if desired, then click "Create". +9. You will now see the Access Key ID and Secret Access Key displayed. Note that these keys are only visible once during creation, so be sure to save them securely. + ![Access Keys](tutorial/access-keys.png) +8. Copy these keys and paste them into your `.env.local` file + +Note: Make sure to keep your keys secure and never share them publicly. + + +## AWS Bedrock RAG Integration + +This project utilizes AWS Bedrock for Retrieval-Augmented Generation (RAG). To set up: + +1. Ensure you have an AWS account with Bedrock access. +2. Create a Bedrock knowledge base in your desired AWS region. +3. Index your documents/sources in the knowledge base. For more info on that, check the "How to Create Your Own Knowledge Base" section. +4. In `ChatArea.tsx`, update the `knowledgeBases` array with your knowledge base IDs and names: + +```typescript +const knowledgeBases: KnowledgeBase[] = [ + { id: "your-knowledge-base-id", name: "Your KB Name" }, + // Add more knowledge bases as needed +]; +``` + +The application will use these knowledge bases for context retrieval during conversations. + +### How to Create Your Own Knowledge Base + +To create your own knowledge base: + +1. Go to your AWS Console and select Amazon Bedrock. +2. In the left side menu, click on "Knowledge base" under "More". + +3. Click on "Create knowledge base". + ![Create Knowledge Base](tutorial/create-knowledge-base.png) +4. Give your knowledge base a name. You can leave "Create a new service role". +5. Choose a source for your knowledge base. In this example, we'll use Amazon S3 storage service. + ![Choose Source](tutorial/choose-source.png) + + Note: If you're using the S3 storage service, you'll need to create a bucket first where you will upload your files. Alternatively, you can also upload your files after the creation of a knowledge base. + +6. Click "Next". +7. Choose a location for your knowledge base. This can be S3 buckets, folders, or even single documents. +8. Click "Next". +9. Select your preferred embedding model. In this case, we'll use Titan Text Embeddings 2. +10. Select "Quick create a new vector store". +11. Confirm and create your knowledge base. +12. Once you have done this, get your knowledge base ID from the knowledge base overview. + + +## Switching Models + +This project supports multiple Claude models. To switch between models: + +1. In `ChatArea.tsx`, the `models` array defines available models: + +```typescript +const models: Model[] = [ + { id: "claude-3-haiku-20240307", name: "Claude 3 Haiku" }, + { id: "claude-3-5-sonnet-20240620", name: "Claude 3.5 Sonnet" }, + // Add more models as needed +]; +``` + +2. The `selectedModel` state variable controls the currently selected model: + +```typescript +const [selectedModel, setSelectedModel] = useState("claude-3-haiku-20240307"); +``` + +3. To implement model switching in the UI, a dropdown component is used that updates the `selectedModel`. + + +## Customization + +This project leverages shadcn/ui components, offering a high degree of customization: + +* Modify the UI components in the `components/ui` directory +* Adjust the theme in `app/globals.css` +* Customize the layout and functionality in individual component files +* Modify the theme colors and styles by editing the `styles/themes.js` file: + +```javascript +// styles/themes.js +export const themes = { + neutral: { + light: { + // Light mode colors for neutral theme + }, + dark: { + // Dark mode colors for neutral theme + } + }, + // Add more themes here +}; +``` +You can add new themes or modify existing ones by adjusting the color values in this file. + +## Deploy with AWS Amplify + +To deploy this application using AWS Amplify, follow these steps: + +1. Go to your AWS Console and select Amplify. +2. Click on "Create new app" (image link to be added later). +3. Select GitHub (or your preferred provider) as the source. +4. Choose this repository. +5. Edit the YAML file to contain: + + ```yaml + version: 1 + frontend: + phases: + preBuild: + commands: + - npm ci --cache .npm --prefer-offline + build: + commands: + - npm run build # Next.js build runs first + - echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env + - echo "KNOWLEDGE_BASE_ID=$KNOWLEDGE_BASE_ID" >> .env + - echo "BAWS_ACCESS_KEY_ID=$BAWS_ACCESS_KEY_ID" >> .env + - echo "BAWS_SECRET_ACCESS_KEY=$BAWS_SECRET_ACCESS_KEY" >> .env + artifacts: + baseDirectory: .next + files: + - "**/*" + cache: + paths: + - .next/cache/**/* + - .npm/**/* + ``` + +6. Choose to create a new service role or use an existing one. Refer to the "Service Role" section for more information. +7. Click on "Advanced settings" and add your environmental variables: + + ``` + ANTHROPIC_API_KEY=your_anthropic_api_key + BAWS_ACCESS_KEY_ID=your_aws_access_key + BAWS_SECRET_ACCESS_KEY=your_aws_secret_key + ``` + The reason we are adding a 'B' in front of the keys here is because AWS doesn't allow keys in Amplify to start with "AWS". + +8. Click "Save and deploy" to start the deployment process. + +Your application will now be deployed using AWS Amplify. + + +### Service Role + +Once your application is deployed, if you selected to create a new service role: + +1. Go to your deployments page +2. Select the deployment you just created +3. Click on "App settings" +4. Copy the Service role ARN +5. Go to the IAM console and find this role +6. Attach the "AmazonBedrockFullAccess" policy to the role + +This ensures that your Amplify app has the necessary permissions to interact with Amazon Bedrock. + +## Customized Deployment and Development +This project now supports flexible deployment and development configurations, allowing you to include or exclude specific components (left sidebar, right sidebar) based on your needs. +Configuration +The inclusion of sidebars is controlled by a config.ts file, which uses environment variables to set the configuration: +```typescript +typescriptCopytype Config = { + includeLeftSidebar: boolean; + includeRightSidebar: boolean; +}; + +const config: Config = { + includeLeftSidebar: process.env.NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR === "true", + includeRightSidebar: process.env.NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR === "true", +}; + +export default config; +``` + +This configuration uses two environment variables: + +NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR: Set to "true" to include the left sidebar +NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR: Set to "true" to include the right sidebar + +## NPM Scripts +The package.json includes several new scripts for different configurations: + +```bash +npm run dev: Runs the full app with both sidebars (default) +npm run build: Builds the full app with both sidebars (default) +npm run dev:full: Same as npm run dev +npm run dev:left: Runs the app with only the left sidebar +npm run dev:right: Runs the app with only the right sidebar +npm run dev:chat: Runs the app with just the chat area (no sidebars) +npm run build:full: Same as npm run build +npm run build:left: Builds the app with only the left sidebar +npm run build:right: Builds the app with only the right sidebar +npm run build:chat: Builds the app with just the chat area (no sidebars) +``` + +Usage +To use a specific configuration: + +For development: Run the desired script (e.g., npm run dev:left) +For production: Build with the desired script (e.g., npm run build:right) + +These scripts set the appropriate environment variables before running or building the application, allowing you to easily switch between different configurations. +This flexibility allows you to tailor the application's layout to your specific needs, whether for testing, development, or production deployment. diff --git a/customer-support-agent/amplify.yml b/customer-support-agent/amplify.yml new file mode 100644 index 00000000..cd0a9824 --- /dev/null +++ b/customer-support-agent/amplify.yml @@ -0,0 +1,21 @@ +version: 1 +frontend: + phases: + preBuild: + commands: + - npm ci --cache .npm --prefer-offline + build: + commands: + - npm run build # Next.js build runs first + - echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env + - echo "KNOWLEDGE_BASE_ID=$KNOWLEDGE_BASE_ID" >> .env + - echo "BAWS_ACCESS_KEY_ID=$BAWS_ACCESS_KEY_ID" >> .env + - echo "BAWS_SECRET_ACCESS_KEY=$BAWS_SECRET_ACCESS_KEY" >> .env + artifacts: + baseDirectory: .next + files: + - "**/*" + cache: + paths: + - .next/cache/**/* + - .npm/**/* diff --git a/customer-support-agent/app/api/chat/route.ts b/customer-support-agent/app/api/chat/route.ts new file mode 100644 index 00000000..8fcaa159 --- /dev/null +++ b/customer-support-agent/app/api/chat/route.ts @@ -0,0 +1,302 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { z } from "zod"; +import { retrieveContext, RAGSource } from "@/app/lib/utils"; +import crypto from "crypto"; +import customerSupportCategories from "@/app/lib/customer_support_categories.json"; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// Debug message helper function +// Input: message string and optional data object +// Output: JSON string with message, sanitized data, and timestamp +const debugMessage = (msg: string, data: any = {}) => { + console.log(msg, data); + const timestamp = new Date().toISOString().replace(/[^\x20-\x7E]/g, ""); + const safeData = JSON.parse(JSON.stringify(data)); + return JSON.stringify({ msg, data: safeData, timestamp }); +}; + +// Define the schema for the AI response using Zod +// This ensures type safety and validation for the AI's output +const responseSchema = z.object({ + response: z.string(), + thinking: z.string(), + user_mood: z.enum([ + "positive", + "neutral", + "negative", + "curious", + "frustrated", + "confused", + ]), + suggested_questions: z.array(z.string()), + debug: z.object({ + context_used: z.boolean(), + }), + matched_categories: z.array(z.string()).optional(), + redirect_to_agent: z + .object({ + should_redirect: z.boolean(), + reason: z.string().optional(), + }) + .optional(), +}); + +// Helper function to sanitize header values +// Input: string value +// Output: sanitized string (ASCII characters only) +function sanitizeHeaderValue(value: string): string { + return value.replace(/[^\x00-\x7F]/g, ""); +} + +// Helper function to log timestamps for performance measurement +// Input: label string and start time +// Output: Logs the duration for the labeled operation +const logTimestamp = (label: string, start: number) => { + const timestamp = new Date().toISOString(); + const time = ((performance.now() - start) / 1000).toFixed(2); + console.log(`⏱️ [${timestamp}] ${label}: ${time}s`); +}; + +// Main POST request handler +export async function POST(req: Request) { + const apiStart = performance.now(); + const measureTime = (label: string) => logTimestamp(label, apiStart); + + // Extract data from the request body + const { messages, model, knowledgeBaseId } = await req.json(); + const latestMessage = messages[messages.length - 1].content; + + console.log("📝 Latest Query:", latestMessage); + measureTime("User Input Received"); + + // Prepare debug data + const MAX_DEBUG_LENGTH = 1000; + const debugData = sanitizeHeaderValue( + debugMessage("🚀 API route called", { + messagesReceived: messages.length, + latestMessageLength: latestMessage.length, + anthropicKeySlice: process.env.ANTHROPIC_API_KEY?.slice(0, 4) + "****", + }), + ).slice(0, MAX_DEBUG_LENGTH); + + // Initialize variables for RAG retrieval + let retrievedContext = ""; + let isRagWorking = false; + let ragSources: RAGSource[] = []; + + // Attempt to retrieve context from RAG + try { + console.log("🔍 Initiating RAG retrieval for query:", latestMessage); + measureTime("RAG Start"); + const result = await retrieveContext(latestMessage, knowledgeBaseId); + retrievedContext = result.context; + isRagWorking = result.isRagWorking; + ragSources = result.ragSources || []; + + if (!result.isRagWorking) { + console.warn("🚨 RAG Retrieval failed but did not throw!"); + } + + measureTime("RAG Complete"); + console.log("🔍 RAG Retrieved:", isRagWorking ? "YES" : "NO"); + console.log( + "✅ RAG retrieval completed successfully. Context:", + retrievedContext.slice(0, 100) + "...", + ); + } catch (error) { + console.error("💀 RAG Error:", error); + console.error("❌ RAG retrieval failed for query:", latestMessage); + retrievedContext = ""; + isRagWorking = false; + ragSources = []; + } + + measureTime("RAG Total Duration"); + + // Prepare categories context for the system prompt + const USE_CATEGORIES = true; + const categoryListString = customerSupportCategories.categories + .map((c) => c.id) + .join(", "); + + const categoriesContext = USE_CATEGORIES + ? ` + To help with our internal classification of inquries, we would like you to categorize inquiries in addition to answering the. We have provided you with ${customerSupportCategories.categories.length} customer support categories. + Check if your response fits into any category and include the category IDs in your "matched_categories" array. + The available categories are: ${categoryListString} + If multiple categories match, include multiple category IDs. If no categories match, return an empty array. + ` + : ""; + + // Change the system prompt company for your use case + const systemPrompt = `You are acting as an Anthropic customer support assistant chatbot inside a chat window on a website. You are chatting with a human user who is asking for help about Anthropic's products and services. When responding to the user, aim to provide concise and helpful responses while maintaining a polite and professional tone. + + To help you answer the user's question, we have retrieved the following information for you. It may or may not be relevant (we are using a RAG pipeline to retrieve this information): + ${isRagWorking ? `${retrievedContext}` : "No information found for this query."} + + Please provide responses that only use the information you have been given. If no information is available or if the information is not relevant for answering the question, you can redirect the user to a human agent for further assistance. + + ${categoriesContext} + + If the question is unrelated to Anthropic's products and services, you should redirect the user to a human agent. + + You are the first point of contact for the user and should try to resolve their issue or provide relevant information. If you are unable to help the user or if the user explicitly asks to talk to a human, you can redirect them to a human agent for further assistance. + + To display your responses correctly, you must format your entire response as a valid JSON object with the following structure: + { + "thinking": "Brief explanation of your reasoning for how you should address the user's query", + "response": "Your concise response to the user", + "user_mood": "positive|neutral|negative|curious|frustrated|confused", + "suggested_questions": ["Question 1?", "Question 2?", "Question 3?"], + "debug": { + "context_used": true|false + }, + ${USE_CATEGORIES ? '"matched_categories": ["category_id1", "category_id2"],' : ""} + "redirect_to_agent": { + "should_redirect": boolean, + "reason": "Reason for redirection (optional, include only if should_redirect is true)" + } + } + + Here are a few examples of how your response should look like: + + Example of a response without redirection to a human agent: + { + "thinking": "Providing relevant information from the knowledge base", + "response": "Here's the information you requested...", + "user_mood": "curious", + "suggested_questions": ["How do I update my account?", "What are the payment options?"], + "debug": { + "context_used": true + }, + "matched_categories": ["account_management", "billing"], + "redirect_to_agent": { + "should_redirect": false + } + } + + Example of a response with redirection to a human agent: + { + "thinking": "User request requires human intervention", + "response": "I understand this is a complex issue. Let me connect you with a human agent who can assist you better.", + "user_mood": "frustrated", + "suggested_questions": [], + "debug": { + "context_used": false + }, + "matched_categories": ["technical_support"], + "redirect_to_agent": { + "should_redirect": true, + "reason": "Complex technical issue requiring human expertise" + } + } + ` + + function sanitizeAndParseJSON(jsonString : string) { + // Replace newlines within string values + const sanitized = jsonString.replace(/(?<=:\s*")(.|\n)*?(?=")/g, match => + match.replace(/\n/g, "\\n") + ); + + try { + return JSON.parse(sanitized); + } catch (parseError) { + console.error("Error parsing JSON response:", parseError); + throw new Error("Invalid JSON response from AI"); + } + } + + try { + console.log(`🚀 Query Processing`); + measureTime("Claude Generation Start"); + + const anthropicMessages = messages.map((msg: any) => ({ + role: msg.role, + content: msg.content, + })); + + anthropicMessages.push({ + role: "assistant", + content: "{", + }); + + const response = await anthropic.messages.create({ + model: model, + max_tokens: 1000, + messages: anthropicMessages, + system: systemPrompt, + temperature: 0.3, + }); + + measureTime("Claude Generation Complete"); + console.log("✅ Message generation completed"); + + // Extract text content from the response + const textContent = "{" + response.content + .filter((block): block is Anthropic.TextBlock => block.type === "text") + .map((block) => block.text) + .join(" "); + + // Parse the JSON response + let parsedResponse; + try { + parsedResponse = sanitizeAndParseJSON(textContent); + } catch (parseError) { + console.error("Error parsing JSON response:", parseError); + throw new Error("Invalid JSON response from AI"); + } + + const validatedResponse = responseSchema.parse(parsedResponse); + + const responseWithId = { + id: crypto.randomUUID(), + ...validatedResponse, + }; + + // Check if redirection to a human agent is needed + if (responseWithId.redirect_to_agent?.should_redirect) { + console.log("🚨 AGENT REDIRECT TRIGGERED!"); + console.log("Reason:", responseWithId.redirect_to_agent.reason); + } + + // Prepare the response object + const apiResponse = new Response(JSON.stringify(responseWithId), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); + + // Add RAG sources to the response headers if available + if (ragSources.length > 0) { + apiResponse.headers.set( + "x-rag-sources", + sanitizeHeaderValue(JSON.stringify(ragSources)), + ); + } + + // Add debug data to the response headers + apiResponse.headers.set("X-Debug-Data", sanitizeHeaderValue(debugData)); + + measureTime("API Complete"); + + return apiResponse; + } catch (error) { + // Handle errors in AI response generation + console.error("💥 Error in message generation:", error); + const errorResponse = { + response: + "Sorry, there was an issue processing your request. Please try again later.", + thinking: "Error occurred during message generation.", + user_mood: "neutral", + debug: { context_used: false }, + }; + return new Response(JSON.stringify(errorResponse), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +} diff --git a/customer-support-agent/app/favicon.ico b/customer-support-agent/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/customer-support-agent/app/globals.css b/customer-support-agent/app/globals.css new file mode 100644 index 00000000..fe163f75 --- /dev/null +++ b/customer-support-agent/app/globals.css @@ -0,0 +1,69 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --radius: 0.75rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/customer-support-agent/app/layout.tsx b/customer-support-agent/app/layout.tsx new file mode 100644 index 00000000..a4fc4b90 --- /dev/null +++ b/customer-support-agent/app/layout.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/components/theme-provider"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "AI Chat Assistant", + description: "Chat with an AI assistant powered by Anthropic", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/customer-support-agent/app/lib/customer_support_categories.json b/customer-support-agent/app/lib/customer_support_categories.json new file mode 100644 index 00000000..92aeac5e --- /dev/null +++ b/customer-support-agent/app/lib/customer_support_categories.json @@ -0,0 +1,92 @@ +{ + "categories": [ + { + "id": "account", + "name": "Account", + "keywords": [ + "ban", + "appeal", + "deletion", + "logging in", + "phone verification", + "roles", + "permissions" + ] + }, + { + "id": "billing", + "name": "Billing", + "keywords": [ + "AR", + "billing details", + "invoice", + "receipt", + "payment method", + "pricing", + "refund request", + "subscription management", + "taxes" + ] + }, + { + "id": "feature", + "name": "Feature", + "keywords": [ + "artifacts", + "chats", + "knowledge bases", + "projects", + "prompt evaluator", + "prompt generator", + "workbench" + ] + }, + { + "id": "internal", + "name": "Internal", + "keywords": [ + "auto-reply", + "forward", + "ignore", + "spam", + "other", + "sales reroute" + ] + }, + { + "id": "legal", + "name": "Legal", + "keywords": ["data privacy", "policy", "terms of service", "usage policy"] + }, + { + "id": "other", + "name": "Other", + "keywords": ["job inquiry", "3rd party company"] + }, + { + "id": "technical", + "name": "Technical", + "keywords": [ + "documentation", + "error message", + "leaked API key", + "outage", + "platform performance", + "security concerns", + "user interface" + ] + }, + { + "id": "usage", + "name": "Usage", + "keywords": [ + "hallucinations", + "model capabilities", + "output quality", + "prompting techniques", + "rate limits", + "regional availability" + ] + } + ] +} diff --git a/customer-support-agent/app/lib/utils.ts b/customer-support-agent/app/lib/utils.ts new file mode 100644 index 00000000..202d2e90 --- /dev/null +++ b/customer-support-agent/app/lib/utils.ts @@ -0,0 +1,95 @@ +import { + BedrockAgentRuntimeClient, + RetrieveCommand, + RetrieveCommandInput, +} from "@aws-sdk/client-bedrock-agent-runtime"; +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +console.log("🔑 Have AWS AccessKey?", !!process.env.BAWS_ACCESS_KEY_ID); +console.log("🔑 Have AWS Secret?", !!process.env.BAWS_SECRET_ACCESS_KEY); + +const bedrockClient = new BedrockAgentRuntimeClient({ + region: "us-east-1", // Make sure this matches your Bedrock region + credentials: { + accessKeyId: process.env.BAWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.BAWS_SECRET_ACCESS_KEY!, + }, +}); + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export interface RAGSource { + id: string; + fileName: string; + snippet: string; + score: number; +} + +export async function retrieveContext( + query: string, + knowledgeBaseId: string, + n: number = 3, +): Promise<{ + context: string; + isRagWorking: boolean; + ragSources: RAGSource[]; +}> { + try { + if (!knowledgeBaseId) { + console.error("knowledgeBaseId is not provided"); + return { + context: "", + isRagWorking: false, + ragSources: [], + }; + } + + const input: RetrieveCommandInput = { + knowledgeBaseId: knowledgeBaseId, + retrievalQuery: { text: query }, + retrievalConfiguration: { + vectorSearchConfiguration: { numberOfResults: n }, + }, + }; + + const command = new RetrieveCommand(input); + const response = await bedrockClient.send(command); + + // Parse results + const rawResults = response?.retrievalResults || []; + const ragSources: RAGSource[] = rawResults + .filter((res: any) => res.content && res.content.text) + .map((result: any, index: number) => { + const uri = result?.location?.s3Location?.uri || ""; + const fileName = uri.split("/").pop() || `Source-${index}.txt`; + + return { + id: + result.metadata?.["x-amz-bedrock-kb-chunk-id"] || `chunk-${index}`, + fileName: fileName.replace(/_/g, " ").replace(".txt", ""), + snippet: result.content?.text || "", + score: result.score || 0, + }; + }) + .slice(0, 1); + + console.log("🔍 Parsed RAG Sources:", ragSources); // Debug log + + const context = rawResults + .filter((res: any) => res.content && res.content.text) + .map((res: any) => res.content.text) + .join("\n\n"); + + return { + context, + isRagWorking: true, + ragSources, + }; + } catch (error) { + console.error("RAG Error:", error); + return { context: "", isRagWorking: false, ragSources: [] }; + } +} diff --git a/customer-support-agent/app/page.tsx b/customer-support-agent/app/page.tsx new file mode 100644 index 00000000..77baa8e7 --- /dev/null +++ b/customer-support-agent/app/page.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import dynamic from "next/dynamic"; +import TopNavBar from "@/components/TopNavBar"; +import ChatArea from "@/components/ChatArea"; +import config from "@/config"; + +const LeftSidebar = dynamic(() => import("@/components/LeftSidebar"), { + ssr: false, +}); +const RightSidebar = dynamic(() => import("@/components/RightSidebar"), { + ssr: false, +}); + +export default function Home() { + return ( +
+ +
+ {config.includeLeftSidebar && } + + {config.includeRightSidebar && } +
+
+ ); +} diff --git a/customer-support-agent/components.json b/customer-support-agent/components.json new file mode 100644 index 00000000..1e879bc7 --- /dev/null +++ b/customer-support-agent/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/customer-support-agent/components/ChatArea.tsx b/customer-support-agent/components/ChatArea.tsx new file mode 100644 index 00000000..8604cbb2 --- /dev/null +++ b/customer-support-agent/components/ChatArea.tsx @@ -0,0 +1,725 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import config from "@/config"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import ReactMarkdown from "react-markdown"; +import rehypeHighlight from "rehype-highlight"; +import rehypeRaw from "rehype-raw"; +import { + HandHelping, + WandSparkles, + LifeBuoyIcon, + BookOpenText, + ChevronDown, + Send, +} from "lucide-react"; +import "highlight.js/styles/atom-one-dark.css"; +import { Card, CardContent, CardFooter } from "@/components/ui/card"; +import { Textarea } from "@/components/ui/textarea"; +import Image from "next/image"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +const TypedText = ({ text = "", delay = 5 }) => { + const [displayedText, setDisplayedText] = useState(""); + + useEffect(() => { + if (!text) return; + const timer = setTimeout(() => { + setDisplayedText(text.substring(0, displayedText.length + 1)); + }, delay); + return () => clearTimeout(timer); + }, [text, displayedText, delay]); + + return <>{displayedText}; +}; + +type ThinkingContent = { + id: string; + content: string; + user_mood: string; + debug: any; + matched_categories?: string[]; +}; + +interface ConversationHeaderProps { + selectedModel: string; + setSelectedModel: (modelId: string) => void; + models: Model[]; + showAvatar: boolean; +} + +const UISelector = ({ + redirectToAgent, +}: { + redirectToAgent: { should_redirect: boolean; reason: string }; +}) => { + if (redirectToAgent.should_redirect) { + return ( + + ); + } + + return null; +}; + +const SuggestedQuestions = ({ + questions, + onQuestionClick, + isLoading, +}: { + questions: string[]; + onQuestionClick: (question: string) => void; + isLoading: boolean; +}) => { + if (!questions || questions.length === 0) return null; + + return ( +
+ {questions.map((question, index) => ( + + ))} +
+ ); +}; + +const MessageContent = ({ + content, + role, +}: { + content: string; + role: string; +}) => { + const [thinking, setThinking] = useState(true); + const [parsed, setParsed] = useState<{ + response?: string; + thinking?: string; + user_mood?: string; + suggested_questions?: string[]; + redirect_to_agent?: { should_redirect: boolean; reason: string }; + debug?: { + context_used: boolean; + }; + }>({}); + const [error, setError] = useState(false); + + useEffect(() => { + if (!content || role !== "assistant") return; + + const timer = setTimeout(() => { + setError(true); + setThinking(false); + }, 30000); + + try { + const result = JSON.parse(content); + console.log("🔍 Parsed Result:", result); + + if ( + result.response && + result.response.length > 0 && + result.response !== "..." + ) { + setParsed(result); + setThinking(false); + clearTimeout(timer); + } + } catch (error) { + console.error("Error parsing JSON:", error); + setError(true); + setThinking(false); + } + + return () => clearTimeout(timer); + }, [content, role]); + + if (thinking && role === "assistant") { + return ( +
+
+ Thinking... +
+ ); + } + + if (error && !parsed.response) { + return
Something went wrong. Please try again.
; + } + + return ( + <> + + {parsed.response || content} + + {parsed.redirect_to_agent && ( + + )} + + ); +}; + +// Define a type for the model +type Model = { + id: string; + name: string; +}; + +interface Message { + id: string; + role: string; + content: string; +} + +// Define the props interface for ConversationHeader +interface ConversationHeaderProps { + selectedModel: string; + setSelectedModel: (modelId: string) => void; + models: Model[]; + showAvatar: boolean; + selectedKnowledgeBase: string; + setSelectedKnowledgeBase: (knowledgeBaseId: string) => void; + knowledgeBases: KnowledgeBase[]; +} + +type KnowledgeBase = { + id: string; + name: string; +}; + +const ConversationHeader: React.FC = ({ + selectedModel, + setSelectedModel, + models, + showAvatar, + selectedKnowledgeBase, + setSelectedKnowledgeBase, + knowledgeBases, +}) => ( +
+
+ {showAvatar && ( + <> + + + AI + +
+

AI Agent

+

Customer support

+
+ + )} +
+
+ + + + + + {models.map((model) => ( + setSelectedModel(model.id)} + > + {model.name} + + ))} + + + + + + + + {knowledgeBases.map((kb) => ( + setSelectedKnowledgeBase(kb.id)} + > + {kb.name} + + ))} + + +
+
+); + +function ChatArea() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [showHeader, setShowHeader] = useState(false); + const [selectedModel, setSelectedModel] = useState("claude-3-haiku-20240307"); + const [showAvatar, setShowAvatar] = useState(false); + + const messagesEndRef = useRef(null); + const [selectedKnowledgeBase, setSelectedKnowledgeBase] = + useState("YPCK2PHDEG"); + + const knowledgeBases: KnowledgeBase[] = [ + { id: "YPCK2PHDEG", name: "Help Docs" }, + { id: "NXR8E9A1RR", name: "Dev Docs" }, + { id: "NKBDJWCEIY", name: "General" }, + // Add more knowledge bases as needed + ]; + + const models: Model[] = [ + { id: "claude-3-haiku-20240307", name: "Claude 3 Haiku" }, + { id: "claude-3-5-sonnet-20240620", name: "Claude 3.5 Sonnet" }, + ]; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + console.log("🔍 Messages changed! Count:", messages.length); + + const scrollToNewestMessage = () => { + if (messagesEndRef.current) { + console.log("📜 Scrolling to newest message..."); + const behavior = messages.length <= 2 ? "auto" : "smooth"; + messagesEndRef.current.scrollIntoView({ behavior, block: "end" }); + } else { + console.log("❌ No scroll anchor found!"); + } + }; + + if (messages.length > 0) { + setTimeout(scrollToNewestMessage, 100); + } + }, [messages]); + + useEffect(() => { + if (!config.includeLeftSidebar) { + // If LeftSidebar is not included, we need to handle the 'updateSidebar' event differently + const handleUpdateSidebar = (event: CustomEvent) => { + console.log("LeftSidebar not included. Event data:", event.detail); + // You might want to handle this data differently when LeftSidebar is not present + }; + + window.addEventListener( + "updateSidebar" as any, + handleUpdateSidebar as EventListener, + ); + return () => + window.removeEventListener( + "updateSidebar" as any, + handleUpdateSidebar as EventListener, + ); + } + }, []); + + useEffect(() => { + if (!config.includeRightSidebar) { + // If RightSidebar is not included, we need to handle the 'updateRagSources' event differently + const handleUpdateRagSources = (event: CustomEvent) => { + console.log("RightSidebar not included. RAG sources:", event.detail); + // You might want to handle this data differently when RightSidebar is not present + }; + + window.addEventListener( + "updateRagSources" as any, + handleUpdateRagSources as EventListener, + ); + return () => + window.removeEventListener( + "updateRagSources" as any, + handleUpdateRagSources as EventListener, + ); + } + }, []); + + const decodeDebugData = (response: Response) => { + const debugData = response.headers.get("X-Debug-Data"); + if (debugData) { + try { + const parsed = JSON.parse(debugData); + console.log("🔍 Server Debug:", parsed.msg, parsed.data); + } catch (e) { + console.error("Debug decode failed:", e); + } + } + }; + + const logDuration = (label: string, duration: number) => { + console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`); + }; + + const handleSubmit = async ( + event: React.FormEvent | string, + ) => { + if (typeof event !== "string") { + event.preventDefault(); + } + if (!showHeader) setShowHeader(true); + if (!showAvatar) setShowAvatar(true); + setIsLoading(true); + + const clientStart = performance.now(); + console.log("🔄 Starting request: " + new Date().toISOString()); + + const userMessage = { + id: crypto.randomUUID(), + role: "user", + content: typeof event === "string" ? event : input, + }; + + const placeholderMessage = { + id: crypto.randomUUID(), + role: "assistant", + content: JSON.stringify({ + response: "", + thinking: "AI is processing...", + user_mood: "neutral", + debug: { + context_used: false, + }, + }), + }; + + setMessages((prevMessages) => [ + ...prevMessages, + userMessage, + placeholderMessage, + ]); + setInput(""); + + const placeholderDisplayed = performance.now(); + logDuration("Perceived Latency", placeholderDisplayed - clientStart); + + try { + console.log("➡️ Sending message to API:", userMessage.content); + const startTime = performance.now(); + const response = await fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + messages: [...messages, userMessage], + model: selectedModel, + knowledgeBaseId: selectedKnowledgeBase, + }), + }); + + const responseReceived = performance.now(); + logDuration("Full Round Trip", responseReceived - startTime); + logDuration("Network Duration", responseReceived - startTime); + + decodeDebugData(response); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + const endTime = performance.now(); + logDuration("JSON Parse Duration", endTime - responseReceived); + logDuration("Total API Duration", endTime - startTime); + console.log("⬅️ Received response from API:", data); + + const suggestedQuestionsHeader = response.headers.get( + "x-suggested-questions", + ); + if (suggestedQuestionsHeader) { + data.suggested_questions = JSON.parse(suggestedQuestionsHeader); + } + + const ragHeader = response.headers.get("x-rag-sources"); + if (ragHeader) { + const ragProcessed = performance.now(); + logDuration( + "🔍 RAG Processing Duration", + ragProcessed - responseReceived, + ); + const sources = JSON.parse(ragHeader); + window.dispatchEvent( + new CustomEvent("updateRagSources", { + detail: { + sources, + query: userMessage.content, + debug: data.debug, + }, + }), + ); + } + + const readyToRender = performance.now(); + logDuration("Response Processing", readyToRender - responseReceived); + + setMessages((prevMessages) => { + const newMessages = [...prevMessages]; + const lastIndex = newMessages.length - 1; + newMessages[lastIndex] = { + id: crypto.randomUUID(), + role: "assistant", + content: JSON.stringify(data), + }; + return newMessages; + }); + + const sidebarEvent = new CustomEvent("updateSidebar", { + detail: { + id: data.id, + content: data.thinking?.trim(), + user_mood: data.user_mood, + debug: data.debug, + matched_categories: data.matched_categories, + }, + }); + window.dispatchEvent(sidebarEvent); + + if (data.redirect_to_agent && data.redirect_to_agent.should_redirect) { + window.dispatchEvent( + new CustomEvent("agentRedirectRequested", { + detail: data.redirect_to_agent, + }), + ); + } + } catch (error) { + console.error("Error fetching chat response:", error); + console.error("Failed to process message:", userMessage.content); + } finally { + setIsLoading(false); + const clientEnd = performance.now(); + logDuration("Total Client Operation", clientEnd - clientStart); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (input.trim() !== "") { + handleSubmit(e as any); + } + } + }; + + const handleInputChange = (event: React.ChangeEvent) => { + const textarea = event.target; + setInput(textarea.value); + + textarea.style.height = "auto"; + textarea.style.height = `${Math.min(textarea.scrollHeight, 300)}px`; + }; + + const handleSuggestedQuestionClick = (question: string) => { + handleSubmit(question); + }; + + useEffect(() => { + const handleToolExecution = (event: Event) => { + const customEvent = event as CustomEvent<{ + ui: { type: string; props: any }; + }>; + console.log("Tool execution event received:", customEvent.detail); + }; + + window.addEventListener("toolExecution", handleToolExecution); + return () => + window.removeEventListener("toolExecution", handleToolExecution); + }, []); + + return ( + + + +
+ {messages.length === 0 ? ( +
+ + + +

+ Here's how I can help +

+
+
+ +

+ Need guidance? I'll help navigate tasks using internal + resources. +

+
+
+ +

+ I'm a whiz at finding information! I can dig through + your knowledge base. +

+
+
+ +

+ I'm always learning! The more you share, the better I + can assist you. +

+
+
+
+ ) : ( +
+ {messages.map((message, index) => ( +
+
+ {message.role === "assistant" && ( + + + AI + + )} +
+ +
+
+ {message.role === "assistant" && ( + + )} +
+ ))} +
+
+ )} +
+ + + +
+