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
JavaScript
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": {