UNPKG

ai-chatbot-react

Version:

A modern, customizable feature-rich AI chatbot component for React applications with voice input, file upload, and Gemini AI integration

1,494 lines (1,474 loc) 140 kB
import require$$1, { useState, useEffect, useRef, useCallback, createContext } from 'react'; import { create } from 'zustand'; import { v4 } from 'uuid'; import { GoogleGenerativeAI } from '@google/generative-ai'; import { X, MessageCircle, BarChart3, Download, MessageSquare, Mic, FileText, Clock, User, Bot, VolumeX, Volume2, Trash2, Check, Copy, ExternalLink, File as File$1, Image as Image$1, Upload, Paperclip, ChevronDown, MicOff, Send, Loader2 } from 'lucide-react'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { motion, AnimatePresence } from 'framer-motion'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var jsxRuntime = {exports: {}}; var reactJsxRuntime_production = {}; /** * @license React * react-jsx-runtime.production.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var hasRequiredReactJsxRuntime_production; function requireReactJsxRuntime_production () { if (hasRequiredReactJsxRuntime_production) return reactJsxRuntime_production; hasRequiredReactJsxRuntime_production = 1; var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); function jsxProd(type, config, maybeKey) { var key = null; void 0 !== maybeKey && (key = "" + maybeKey); void 0 !== config.key && (key = "" + config.key); if ("key" in config) { maybeKey = {}; for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]); } else maybeKey = config; config = maybeKey.ref; return { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: void 0 !== config ? config : null, props: maybeKey }; } reactJsxRuntime_production.Fragment = REACT_FRAGMENT_TYPE; reactJsxRuntime_production.jsx = jsxProd; reactJsxRuntime_production.jsxs = jsxProd; return reactJsxRuntime_production; } var reactJsxRuntime_development = {}; /** * @license React * react-jsx-runtime.development.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var hasRequiredReactJsxRuntime_development; function requireReactJsxRuntime_development () { if (hasRequiredReactJsxRuntime_development) return reactJsxRuntime_development; hasRequiredReactJsxRuntime_development = 1; "production" !== process.env.NODE_ENV && (function () { function getComponentNameFromType(type) { if (null == type) return null; if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null; if ("string" === typeof type) return type; switch (type) { case REACT_FRAGMENT_TYPE: return "Fragment"; case REACT_PROFILER_TYPE: return "Profiler"; case REACT_STRICT_MODE_TYPE: return "StrictMode"; case REACT_SUSPENSE_TYPE: return "Suspense"; case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList"; case REACT_ACTIVITY_TYPE: return "Activity"; } if ("object" === typeof type) switch ( ("number" === typeof type.tag && console.error( "Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue." ), type.$$typeof) ) { case REACT_PORTAL_TYPE: return "Portal"; case REACT_CONTEXT_TYPE: return (type.displayName || "Context") + ".Provider"; case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer"; case REACT_FORWARD_REF_TYPE: var innerType = type.render; type = type.displayName; type || ((type = innerType.displayName || innerType.name || ""), (type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef")); return type; case REACT_MEMO_TYPE: return ( (innerType = type.displayName || null), null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo" ); case REACT_LAZY_TYPE: innerType = type._payload; type = type._init; try { return getComponentNameFromType(type(innerType)); } catch (x) {} } return null; } function testStringCoercion(value) { return "" + value; } function checkKeyStringCoercion(value) { try { testStringCoercion(value); var JSCompiler_inline_result = !1; } catch (e) { JSCompiler_inline_result = true; } if (JSCompiler_inline_result) { JSCompiler_inline_result = console; var JSCompiler_temp_const = JSCompiler_inline_result.error; var JSCompiler_inline_result$jscomp$0 = ("function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag]) || value.constructor.name || "Object"; JSCompiler_temp_const.call( JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0 ); return testStringCoercion(value); } } function getTaskName(type) { if (type === REACT_FRAGMENT_TYPE) return "<>"; if ( "object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE ) return "<...>"; try { var name = getComponentNameFromType(type); return name ? "<" + name + ">" : "<...>"; } catch (x) { return "<...>"; } } function getOwner() { var dispatcher = ReactSharedInternals.A; return null === dispatcher ? null : dispatcher.getOwner(); } function UnknownOwner() { return Error("react-stack-top-frame"); } function hasValidKey(config) { if (hasOwnProperty.call(config, "key")) { var getter = Object.getOwnPropertyDescriptor(config, "key").get; if (getter && getter.isReactWarning) return false; } return void 0 !== config.key; } function defineKeyPropWarningGetter(props, displayName) { function warnAboutAccessingKey() { specialPropKeyWarningShown || ((specialPropKeyWarningShown = true), console.error( "%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName )); } warnAboutAccessingKey.isReactWarning = true; Object.defineProperty(props, "key", { get: warnAboutAccessingKey, configurable: true }); } function elementRefGetterWithDeprecationWarning() { var componentName = getComponentNameFromType(this.type); didWarnAboutElementRef[componentName] || ((didWarnAboutElementRef[componentName] = true), console.error( "Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release." )); componentName = this.props.ref; return void 0 !== componentName ? componentName : null; } function ReactElement( type, key, self, source, owner, props, debugStack, debugTask ) { self = props.ref; type = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, props: props, _owner: owner }; null !== (void 0 !== self ? self : null) ? Object.defineProperty(type, "ref", { enumerable: false, get: elementRefGetterWithDeprecationWarning }) : Object.defineProperty(type, "ref", { enumerable: false, value: null }); type._store = {}; Object.defineProperty(type._store, "validated", { configurable: false, enumerable: false, writable: true, value: 0 }); Object.defineProperty(type, "_debugInfo", { configurable: false, enumerable: false, writable: true, value: null }); Object.defineProperty(type, "_debugStack", { configurable: false, enumerable: false, writable: true, value: debugStack }); Object.defineProperty(type, "_debugTask", { configurable: false, enumerable: false, writable: true, value: debugTask }); Object.freeze && (Object.freeze(type.props), Object.freeze(type)); return type; } function jsxDEVImpl( type, config, maybeKey, isStaticChildren, source, self, debugStack, debugTask ) { var children = config.children; if (void 0 !== children) if (isStaticChildren) if (isArrayImpl(children)) { for ( isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++ ) validateChildKeys(children[isStaticChildren]); Object.freeze && Object.freeze(children); } else console.error( "React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead." ); else validateChildKeys(children); if (hasOwnProperty.call(config, "key")) { children = getComponentNameFromType(type); var keys = Object.keys(config).filter(function (k) { return "key" !== k; }); isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}"; didWarnAboutKeySpread[children + isStaticChildren] || ((keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}"), console.error( 'A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', isStaticChildren, children, keys, children ), (didWarnAboutKeySpread[children + isStaticChildren] = true)); } children = null; void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), (children = "" + maybeKey)); hasValidKey(config) && (checkKeyStringCoercion(config.key), (children = "" + config.key)); if ("key" in config) { maybeKey = {}; for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]); } else maybeKey = config; children && defineKeyPropWarningGetter( maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type ); return ReactElement( type, children, self, source, getOwner(), maybeKey, debugStack, debugTask ); } function validateChildKeys(node) { "object" === typeof node && null !== node && node.$$typeof === REACT_ELEMENT_TYPE && node._store && (node._store.validated = 1); } var React = require$$1, REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"); var REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function () { return null; }; React = { react_stack_bottom_frame: function (callStackForError) { return callStackForError(); } }; var specialPropKeyWarningShown; var didWarnAboutElementRef = {}; var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind( React, UnknownOwner )(); var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner)); var didWarnAboutKeySpread = {}; reactJsxRuntime_development.Fragment = REACT_FRAGMENT_TYPE; reactJsxRuntime_development.jsx = function (type, config, maybeKey, source, self) { var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++; return jsxDEVImpl( type, config, maybeKey, false, source, self, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask ); }; reactJsxRuntime_development.jsxs = function (type, config, maybeKey, source, self) { var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++; return jsxDEVImpl( type, config, maybeKey, true, source, self, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask ); }; })(); return reactJsxRuntime_development; } if (process.env.NODE_ENV === 'production') { jsxRuntime.exports = requireReactJsxRuntime_production(); } else { jsxRuntime.exports = requireReactJsxRuntime_development(); } var jsxRuntimeExports = jsxRuntime.exports; function createJSONStorage(getStorage, options) { let storage; try { storage = getStorage(); } catch (_e) { return; } const persistStorage = { getItem: (name) => { var _a; const parse = (str2) => { if (str2 === null) { return null; } return JSON.parse(str2, void 0 ); }; const str = (_a = storage.getItem(name)) != null ? _a : null; if (str instanceof Promise) { return str.then(parse); } return parse(str); }, setItem: (name, newValue) => storage.setItem( name, JSON.stringify(newValue, void 0 ) ), removeItem: (name) => storage.removeItem(name) }; return persistStorage; } const toThenable = (fn) => (input) => { try { const result = fn(input); if (result instanceof Promise) { return result; } return { then(onFulfilled) { return toThenable(onFulfilled)(result); }, catch(_onRejected) { return this; } }; } catch (e) { return { then(_onFulfilled) { return this; }, catch(onRejected) { return toThenable(onRejected)(e); } }; } }; const oldImpl = (config, baseOptions) => (set, get, api) => { let options = { getStorage: () => localStorage, serialize: JSON.stringify, deserialize: JSON.parse, partialize: (state) => state, version: 0, merge: (persistedState, currentState) => ({ ...currentState, ...persistedState }), ...baseOptions }; let hasHydrated = false; const hydrationListeners = /* @__PURE__ */ new Set(); const finishHydrationListeners = /* @__PURE__ */ new Set(); let storage; try { storage = options.getStorage(); } catch (_e) { } if (!storage) { return config( (...args) => { console.warn( `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.` ); set(...args); }, get, api ); } const thenableSerialize = toThenable(options.serialize); const setItem = () => { const state = options.partialize({ ...get() }); let errorInSync; const thenable = thenableSerialize({ state, version: options.version }).then( (serializedValue) => storage.setItem(options.name, serializedValue) ).catch((e) => { errorInSync = e; }); if (errorInSync) { throw errorInSync; } return thenable; }; const savedSetState = api.setState; api.setState = (state, replace) => { savedSetState(state, replace); void setItem(); }; const configResult = config( (...args) => { set(...args); void setItem(); }, get, api ); let stateFromStorage; const hydrate = () => { var _a; if (!storage) return; hasHydrated = false; hydrationListeners.forEach((cb) => cb(get())); const postRehydrationCallback = ((_a = options.onRehydrateStorage) == null ? void 0 : _a.call(options, get())) || void 0; return toThenable(storage.getItem.bind(storage))(options.name).then((storageValue) => { if (storageValue) { return options.deserialize(storageValue); } }).then((deserializedStorageValue) => { if (deserializedStorageValue) { if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) { if (options.migrate) { return options.migrate( deserializedStorageValue.state, deserializedStorageValue.version ); } console.error( `State loaded from storage couldn't be migrated since no migrate function was provided` ); } else { return deserializedStorageValue.state; } } }).then((migratedState) => { var _a2; stateFromStorage = options.merge( migratedState, (_a2 = get()) != null ? _a2 : configResult ); set(stateFromStorage, true); return setItem(); }).then(() => { postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0); hasHydrated = true; finishHydrationListeners.forEach((cb) => cb(stateFromStorage)); }).catch((e) => { postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e); }); }; api.persist = { setOptions: (newOptions) => { options = { ...options, ...newOptions }; if (newOptions.getStorage) { storage = newOptions.getStorage(); } }, clearStorage: () => { storage == null ? void 0 : storage.removeItem(options.name); }, getOptions: () => options, rehydrate: () => hydrate(), hasHydrated: () => hasHydrated, onHydrate: (cb) => { hydrationListeners.add(cb); return () => { hydrationListeners.delete(cb); }; }, onFinishHydration: (cb) => { finishHydrationListeners.add(cb); return () => { finishHydrationListeners.delete(cb); }; } }; hydrate(); return stateFromStorage || configResult; }; const newImpl = (config, baseOptions) => (set, get, api) => { let options = { storage: createJSONStorage(() => localStorage), partialize: (state) => state, version: 0, merge: (persistedState, currentState) => ({ ...currentState, ...persistedState }), ...baseOptions }; let hasHydrated = false; const hydrationListeners = /* @__PURE__ */ new Set(); const finishHydrationListeners = /* @__PURE__ */ new Set(); let storage = options.storage; if (!storage) { return config( (...args) => { console.warn( `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.` ); set(...args); }, get, api ); } const setItem = () => { const state = options.partialize({ ...get() }); return storage.setItem(options.name, { state, version: options.version }); }; const savedSetState = api.setState; api.setState = (state, replace) => { savedSetState(state, replace); void setItem(); }; const configResult = config( (...args) => { set(...args); void setItem(); }, get, api ); api.getInitialState = () => configResult; let stateFromStorage; const hydrate = () => { var _a, _b; if (!storage) return; hasHydrated = false; hydrationListeners.forEach((cb) => { var _a2; return cb((_a2 = get()) != null ? _a2 : configResult); }); const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0; return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => { if (deserializedStorageValue) { if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) { if (options.migrate) { return [ true, options.migrate( deserializedStorageValue.state, deserializedStorageValue.version ) ]; } console.error( `State loaded from storage couldn't be migrated since no migrate function was provided` ); } else { return [false, deserializedStorageValue.state]; } } return [false, void 0]; }).then((migrationResult) => { var _a2; const [migrated, migratedState] = migrationResult; stateFromStorage = options.merge( migratedState, (_a2 = get()) != null ? _a2 : configResult ); set(stateFromStorage, true); if (migrated) { return setItem(); } }).then(() => { postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0); stateFromStorage = get(); hasHydrated = true; finishHydrationListeners.forEach((cb) => cb(stateFromStorage)); }).catch((e) => { postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e); }); }; api.persist = { setOptions: (newOptions) => { options = { ...options, ...newOptions }; if (newOptions.storage) { storage = newOptions.storage; } }, clearStorage: () => { storage == null ? void 0 : storage.removeItem(options.name); }, getOptions: () => options, rehydrate: () => hydrate(), hasHydrated: () => hasHydrated, onHydrate: (cb) => { hydrationListeners.add(cb); return () => { hydrationListeners.delete(cb); }; }, onFinishHydration: (cb) => { finishHydrationListeners.add(cb); return () => { finishHydrationListeners.delete(cb); }; } }; if (!options.skipHydration) { hydrate(); } return stateFromStorage || configResult; }; const persistImpl = (config, baseOptions) => { if ("getStorage" in baseOptions || "serialize" in baseOptions || "deserialize" in baseOptions) { if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") { console.warn( "[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead." ); } return oldImpl(config, baseOptions); } return newImpl(config, baseOptions); }; const persist = persistImpl; // Local Storage keys const STORAGE_KEYS = { CHAT_HISTORY: "portfolio-chatbot-history", CONFIG: "portfolio-chatbot-config", ANALYTICS: "portfolio-chatbot-analytics", }; // Default configuration const DEFAULT_CONFIG = { geminiApiKey: "", position: { desktop: { bottom: "2rem", right: "2rem", }, mobile: { bottom: "1rem", right: "1rem", }, }, title: "Chat with me", subtitle: "Ask me anything about my work", placeholder: "Type your message...", welcomeMessage: "Hello! I'm here to help. What would you like to know about my work?", name: "AI Assistant", avatar: "", enableVoiceInput: true, enableConversationHistory: true, maxHistoryLength: 50, enableFileUpload: true, enableRichFormatting: true, enableMarkdown: true, enableAnalytics: false, customPrompt: "", systemMessage: "You are a helpful AI assistant for a portfolio website. Be friendly, professional, and concise in your responses.", temperature: 0.7, maxTokens: 1000, model: "gemini-2.0-flash", maxFileSize: 10, // 10MB allowedFileTypes: [ ".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".txt", ], enableImageUpload: true, enableDocumentUpload: true, theme: "auto", primaryColor: "#3b82f6", accentColor: "#1d4ed8", borderRadius: "0.5rem", fontSize: "14px", enableUsageTracking: false, analyticsEndpoint: "", }; class StorageManager { static isAvailable() { try { const test = "__storage_test__"; localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch { return false; } } static saveChatHistory(messages) { if (!this.isAvailable()) { console.warn("localStorage is not available"); return; } try { const serializedMessages = messages.map(msg => ({ ...msg, timestamp: msg.timestamp.toISOString(), })); localStorage.setItem(STORAGE_KEYS.CHAT_HISTORY, JSON.stringify(serializedMessages)); } catch (error) { console.error("Failed to save chat history:", error); } } static loadChatHistory() { if (!this.isAvailable()) { console.warn("localStorage is not available"); return []; } try { const stored = localStorage.getItem(STORAGE_KEYS.CHAT_HISTORY); if (!stored) return []; const parsedMessages = JSON.parse(stored); return parsedMessages.map((msg) => ({ ...msg, timestamp: new Date(msg.timestamp), })); } catch (error) { console.error("Failed to load chat history:", error); return []; } } static clearChatHistory() { if (!this.isAvailable()) { console.warn("localStorage is not available"); return; } try { localStorage.removeItem(STORAGE_KEYS.CHAT_HISTORY); } catch (error) { console.error("Failed to clear chat history:", error); } } static saveConfig(config) { if (!this.isAvailable()) { console.warn("localStorage is not available"); return; } try { localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(config)); } catch (error) { console.error("Failed to save config:", error); } } static loadConfig() { if (!this.isAvailable()) { console.warn("localStorage is not available"); return null; } try { const stored = localStorage.getItem(STORAGE_KEYS.CONFIG); return stored ? JSON.parse(stored) : null; } catch (error) { console.error("Failed to load config:", error); return null; } } } class GeminiService { constructor(apiKey, config) { if (!apiKey) { throw new Error("Gemini API key is required"); } this.config = config || {}; this.genAI = new GoogleGenerativeAI(apiKey); this.model = this.genAI.getGenerativeModel({ model: config?.model || "gemini-2.0-flash", }); this.chat = this.model.startChat({ history: [], generationConfig: { maxOutputTokens: config?.maxTokens || 1000, temperature: config?.temperature || 0.7, }, }); } formatMessagesForGemini(messages) { return messages.map(message => ({ role: message.role === "user" ? "user" : "model", parts: [{ text: message.content }], })); } async sendMessage(content, conversationHistory = [], customPrompt) { try { const startTime = Date.now(); // Format conversation history for Gemini const formattedHistory = this.formatMessagesForGemini(conversationHistory); // Prepare the message content with custom prompt if provided let messageContent = content; if (customPrompt || this.config.customPrompt) { messageContent = `${customPrompt || this.config.customPrompt}\n\nUser: ${content}`; } // Add system message if configured if (this.config.systemMessage) { formattedHistory.unshift({ role: "user", parts: [{ text: `System: ${this.config.systemMessage}` }], }); } // Start a new chat with the conversation history this.chat = this.model.startChat({ history: formattedHistory, generationConfig: { maxOutputTokens: this.config.maxTokens || 1000, temperature: this.config.temperature || 0.7, }, }); // Send the current message const result = await this.chat.sendMessage(messageContent); const response = await result.response; const text = response.text(); const processingTime = Date.now() - startTime; return { text, usage: { promptTokens: result.response.usageMetadata?.promptTokenCount || 0, responseTokens: result.response.usageMetadata?.candidatesTokenCount || 0, totalTokens: result.response.usageMetadata?.totalTokenCount || 0, }, metadata: { processingTime, model: this.config.model || "gemini-2.0-flash", temperature: this.config.temperature || 0.7, maxTokens: this.config.maxTokens || 1000, }, }; } catch (error) { console.error("Gemini API error:", error); const apiError = { message: this.handleGeminiError(error), code: error.code || "UNKNOWN_ERROR", status: error.status || 500, }; throw apiError; } } handleGeminiError(error) { // Handle specific Gemini API errors if (error.message?.includes("API_KEY_INVALID")) { return "Invalid API key. Please check your Gemini API key."; } if (error.message?.includes("QUOTA_EXCEEDED")) { return "API quota exceeded. Please try again later."; } if (error.message?.includes("SAFETY")) { return "Message blocked due to safety concerns. Please rephrase your question."; } if (error.message?.includes("BLOCKED")) { return "Message blocked. Please try a different approach."; } if (error.status === 429) { return "Rate limit exceeded. Please wait a moment before trying again."; } if (error.status >= 500) { return "Server error. Please try again later."; } return error.message || "An unexpected error occurred. Please try again."; } async validateApiKey(apiKey) { try { const testGenAI = new GoogleGenerativeAI(apiKey); const testModel = testGenAI.getGenerativeModel({ model: "gemini-2.0-flash", }); const testChat = testModel.startChat(); const result = await testChat.sendMessage("Hello"); await result.response.text(); return true; } catch (error) { console.error("API key validation failed:", error); return false; } } } class MicrophoneService { constructor() { this.mediaRecorder = null; this.audioChunks = []; this.stream = null; this.recognition = null; this.isSupported = this.checkSupport(); this.initializeSpeechRecognition(); } checkSupport() { const SpeechRecognition = globalThis.SpeechRecognition || globalThis.webkitSpeechRecognition; return !!(navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && SpeechRecognition && typeof SpeechRecognition === "function"); } initializeSpeechRecognition() { if (!this.isSupported) return; const SpeechRecognition = globalThis.SpeechRecognition || globalThis.webkitSpeechRecognition; if (!SpeechRecognition) { console.warn("SpeechRecognition is not available"); return; } this.recognition = new SpeechRecognition(); this.recognition.continuous = true; this.recognition.interimResults = true; this.recognition.lang = "en-US"; this.recognition.onstart = () => { console.log("Speech recognition started"); }; this.recognition.addEventListener("error", (event) => { console.error("Speech recognition error:", event.error); }); this.recognition.onend = () => { console.log("Speech recognition ended"); }; } async requestPermission() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); for (const track of stream.getTracks()) track.stop(); return true; } catch (error) { console.error("Microphone permission denied:", error); return false; } } async startListening(onTranscript, onError) { if (!this.isSupported) { onError("Speech recognition is not supported in this browser"); return this.getCurrentState(); } const hasPermission = await this.requestPermission(); if (!hasPermission) { onError("Microphone permission is required for voice input"); return this.getCurrentState(); } if (!this.recognition) { onError("Speech recognition is not available"); return this.getCurrentState(); } try { this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this.recognition.onresult = (event) => { let finalTranscript = ""; let interimTranscript = ""; for (let index = event.resultIndex; index < event.results.length; index++) { const transcript = event.results[index][0].transcript; if (event.results[index].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } if (finalTranscript) { onTranscript(finalTranscript, true); } else if (interimTranscript) { onTranscript(interimTranscript, false); } }; this.recognition.start(); return { isListening: true, isPaused: false, isPermissionGranted: true, isSupported: this.isSupported, }; } catch (error) { console.error("Failed to start listening:", error); onError(this.handleMicrophoneError(error)); return this.getCurrentState(); } } stopListening() { if (this.recognition) { this.recognition.stop(); } if (this.stream) { for (const track of this.stream.getTracks()) track.stop(); this.stream = null; } return { isListening: false, isPaused: false, isPermissionGranted: this.stream !== null, isSupported: this.isSupported, }; } pauseListening() { if (this.recognition) { this.recognition.stop(); } return { isListening: false, isPaused: true, isPermissionGranted: this.stream !== null, isSupported: this.isSupported, }; } resumeListening(onTranscript, onError) { return this.startListening(onTranscript, onError); } handleMicrophoneError(error) { if (error.name === "NotAllowedError") { return "Microphone access was denied. Please allow microphone access and try again."; } if (error.name === "NotFoundError") { return "No microphone found. Please connect a microphone and try again."; } if (error.name === "NotReadableError") { return "Microphone is already in use by another application."; } if (error.name === "NetworkError") { return "Network error occurred. Please check your internet connection."; } return error.message || "An error occurred while accessing the microphone."; } getCurrentState() { return { isListening: this.recognition?.state === "recording", isPaused: this.recognition?.state === "inactive" && this.stream !== null, isPermissionGranted: this.stream !== null, isSupported: this.isSupported, }; } cleanup() { this.stopListening(); this.recognition = null; } } class AnalyticsService { constructor() { this.events = []; this.sessionId = this.generateSessionId(); this.startTime = new Date(); this.loadEvents(); } generateSessionId() { return `session_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; } getSessionId() { return this.sessionId; } loadEvents() { try { const stored = localStorage.getItem(STORAGE_KEYS.ANALYTICS); if (stored) { const parsed = JSON.parse(stored); this.events = parsed.events?.map((event) => ({ ...event, timestamp: new Date(event.timestamp), })) || []; } } catch (error) { console.error("Failed to load analytics:", error); this.events = []; } } saveEvents() { try { const data = { sessionId: this.sessionId, events: this.events, lastUpdated: new Date().toISOString(), }; localStorage.setItem(STORAGE_KEYS.ANALYTICS, JSON.stringify(data)); } catch (error) { console.error("Failed to save analytics:", error); } } trackEvent(event, data) { const analyticsEvent = { event, timestamp: new Date(), data, sessionId: this.sessionId, }; this.events.push(analyticsEvent); this.saveEvents(); // Send to external analytics if configured this.sendToExternalAnalytics(analyticsEvent); } async sendToExternalAnalytics(event) { // This would be implemented to send data to external analytics services // For now, we just log it if (process.env.NODE_ENV === "development") { console.log("Analytics Event:", event); } } getAnalytics() { const now = new Date(); const messageEvents = this.events.filter(e => e.event === "message_sent"); const voiceEvents = this.events.filter(e => e.event === "voice_message"); const fileEvents = this.events.filter(e => e.event === "file_upload"); const responseTimeEvents = this.events .filter(e => e.event === "ai_response") .map(e => e.data?.processingTime || 0); const totalTokens = this.events .filter(e => e.event === "ai_response") .reduce((sum, e) => sum + (e.data?.tokens || 0), 0); return { totalMessages: messageEvents.length, totalTokens, averageResponseTime: responseTimeEvents.length > 0 ? responseTimeEvents.reduce((sum, time) => sum + time, 0) / responseTimeEvents.length : 0, voiceMessages: voiceEvents.length, fileUploads: fileEvents.length, sessionStartTime: this.startTime, lastActivity: now, events: this.events, sessionId: this.sessionId, }; } trackMessageSent(isVoice = false, attachments) { this.trackEvent("message_sent", { isVoice, hasAttachments: attachments && attachments.length > 0, attachmentCount: attachments?.length || 0, }); } trackMessage(role, content, tokens) { this.trackEvent(role === "user" ? "message_sent" : "message_received", { role, content, tokens, }); } trackVoiceInput(action, error) { this.trackEvent(`voice_input_${action}`, { action, error, }); } trackAIResponse(processingTime, tokens, model) { this.trackEvent("ai_response", { processingTime, tokens, model, }); } trackFileUpload(fileType, fileSize) { this.trackEvent("file_upload", { fileType, fileSize, }); } trackError(error, context) { this.trackEvent("error", { error, context, }); } trackSessionStart() { this.trackEvent("session_start", { userAgent: navigator.userAgent, screenSize: `${window.screen.width}x${window.screen.height}`, }); } trackSessionEnd() { this.trackEvent("session_end", { duration: Date.now() - this.startTime.getTime(), }); } clearAnalytics() { this.events = []; localStorage.removeItem(STORAGE_KEYS.ANALYTICS); } exportAnalytics() { return JSON.stringify({ sessionId: this.sessionId, analytics: this.getAnalytics(), events: this.events, exportTime: new Date().toISOString(), }, null, 2); } } class FileUploadService { constructor(config) { this.config = config; } validateFile(file) { // Check file size const maxSize = (this.config.maxFileSize || 10) * 1024 * 1024; // Convert MB to bytes if (file.size > maxSize) { return { isValid: false, error: `File size exceeds maximum allowed size of ${this.config.maxFileSize}MB`, }; } // Check file type const allowedTypes = this.config.allowedFileTypes || []; const fileExtension = `.${file.name.split(".").pop()?.toLowerCase()}`; if (allowedTypes.length > 0 && !allowedTypes.includes(fileExtension)) { return { isValid: false, error: `File type ${fileExtension} is not allowed. Allowed types: ${allowedTypes.join(", ")}`, }; } // Check if image upload is enabled for images if (this.isImageFile(file) && !this.config.enableImageUpload) { return { isValid: false, error: "Image upload is not enabled", }; } // Check if document upload is enabled for documents if (this.isDocumentFile(file) && !this.config.enableDocumentUpload) { return { isValid: false, error: "Document upload is not enabled", }; } return { isValid: true }; } isImageFile(file) { return file.type.startsWith("image/"); } isDocumentFile(file) { return (file.type.startsWith("application/") || file.type.startsWith("text/") || file.name.endsWith(".pdf") || file.name.endsWith(".doc") || file.name.endsWith(".docx")); } async uploadFile(file, onProgress) { const validation = this.validateFile(file); if (!validation.isValid) { throw new Error(validation.error); } try { // For demo purposes, we'll simulate file upload // In a real implementation, you would upload to your server or cloud storage const attachment = await this.simulateFileUpload(file, onProgress); return attachment; } catch (error) { throw new Error(`Failed to upload file: ${error}`); } } async simulateFileUpload(file, onProgress) { return new Promise(resolve => { let progress = 0; const interval = setInterval(() => { progress += Math.random() * 20; if (progress >= 100) { progress = 100; clearInterval(interval); // Simulate successful upload const attachment = { id: `file_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, name: file.name, type: this.getFileType(file), url: URL.createObjectURL(file), // In real implementation, this would be the uploaded URL size: file.size, mimeType: file.type, }; resolve(attachment); } onProgress?.(progress); }, 100); }); } getFileType(file) { if (file.type.startsWith("image/")) return "image"; if (file.type.startsWith("video/")) return "video"; if (file.type.startsWith("audio/")) return "audio"; return "document"; } async uploadToServer(file, endpoint, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", file); xhr.upload.addEventListener("progress", event => { if (event.lengthComputable) { const progress = (event.loaded / event.total) * 100; onProgress?.(progress); } }); xhr.addEventListener("load", () => { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); resolve(response); } catch { reject(new Error("Invalid response format")); } } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }); xhr.addEventListener("error", () => { reject(new Error("Upload failed")); }); xhr.open("POST", endpoint); xhr.send(formData); }); } formatFileSize(bytes) { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const index = Math.floor(Math.log(bytes) / Math.log(k)); return (Number.parseFloat((bytes / Math.pow(k, index)).toFixed(2)) + " " + sizes[index]); } getFileIcon(fileType) { switch (fileType) { case "image": { return "🖼️"; } case "document": { return "📄"; } case "audio": {