diff --git a/README.md b/README.md
index 8b4a1fe..c0482e3 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-[](https://github.com/Samagra-Development/ChatUI/blob/next/LICENSE) [](https://www.npmjs.com/package/samagra-chatui)
+[](https://github.com/Samagra-Development/ChatUI/blob/next/LICENSE) [](https://www.npmjs.com/package/@samagra-x/chatui)
@@ -31,18 +31,18 @@
## Install
```bash
-npm install samagra-chatui --save
+npm install @samagra-x/chatui
```
```bash
-yarn add samagra-chatui
+yarn add @samagra-x/chatui
```
## Usage
```jsx
-import Chat, { Bubble, useMessages } from 'samagra-chatui';
-import 'samagra-chatui/dist/index.css';
+import Chat, { Bubble, useMessages } from '@samagra-x/chatui';
+import '@samagra-x/chatui/dist/index.css';
const App = () => {
const { messages, appendMsg, setTyping } = useMessages([]);
diff --git a/package.json b/package.json
index a16a5da..ff9e1d5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@samagra-x/chatui",
- "version": "1.0.1",
+ "version": "1.0.3",
"description": "The React library for Chatbot UI",
"main": "lib/index.js",
"module": "es/index.js",
diff --git a/src/components/Chat/index.tsx b/src/components/Chat/index.tsx
index 624d126..72e5e37 100644
--- a/src/components/Chat/index.tsx
+++ b/src/components/Chat/index.tsx
@@ -10,6 +10,14 @@ import { QuickReplies, QuickReplyItemProps } from '../QuickReplies';
import { Composer as DComposer, ComposerProps, ComposerHandle } from '../Composer';
import { isSafari, getIOSMajorVersion } from '../../utils/ua';
+export type TransliterationConfig = {
+ transliterationApi:string;
+ transliterationSuggestions?:number;
+ transliterationInputLanguage:string;
+ transliterationOutputLanguage:string;
+ transliterationProvider?:string;
+}
+
export type ChatProps = Omit &
MessageContainerProps & {
/**
@@ -145,6 +153,8 @@ export type ChatProps = Omit &
disableSend?:boolean
btnColor?:string
background?:string
+ showTransliteration?:boolean
+ transliterationConfig?:TransliterationConfig
};
export const Chat = React.forwardRef((props, ref) => {
@@ -178,6 +188,8 @@ export const Chat = React.forwardRef((props, ref) =>
onInputBlur,
onSend,
disableSend,
+ showTransliteration,
+ transliterationConfig,
btnColor,
background,
onImageSend,
@@ -262,6 +274,8 @@ export const Chat = React.forwardRef((props, ref) =>
onBlur={onInputBlur}
onSend={onSend}
disableSend={disableSend}
+ showTransliteration={showTransliteration}
+ transliterationConfig={transliterationConfig}
btnColor={btnColor}
onImageSend={onImageSend}
rightAction={rightAction}
diff --git a/src/components/Composer/ComposerInput.tsx b/src/components/Composer/ComposerInput.tsx
index 034b17a..7969d76 100644
--- a/src/components/Composer/ComposerInput.tsx
+++ b/src/components/Composer/ComposerInput.tsx
@@ -5,6 +5,7 @@ import { SendConfirm } from '../SendConfirm';
import riseInput from './riseInput';
import parseDataTransfer from '../../utils/parseDataTransfer';
import canUse from '../../utils/canUse';
+import { TransliterationConfig } from '../Chat';
const canTouch = canUse('touch');
@@ -12,6 +13,10 @@ interface ComposerInputProps extends InputProps {
invisible: boolean;
inputRef: React.MutableRefObject;
onImageSend?: (file: File) => Promise;
+ showTransliteration: boolean;
+ transliterationConfig: TransliterationConfig | null;
+ cursorPosition: number;
+ setCursorPosition: any;
}
export const ComposerInput = ({
@@ -19,9 +24,18 @@ export const ComposerInput = ({
invisible,
onImageSend,
disabled,
+ showTransliteration,
+ transliterationConfig,
+ value,
+ onChange,
+ cursorPosition,
+ setCursorPosition,
...rest
}: ComposerInputProps) => {
const [pastedImage, setPastedImage] = useState(null);
+ const [suggestions, setSuggestions] = useState([]);
+ const [suggestionClicked, setSuggestionClicked] = useState(false);
+ const [activeSuggestion, setActiveSuggestion] = useState(0);
const handlePaste = useCallback((e: React.ClipboardEvent) => {
parseDataTransfer(e, setPastedImage);
@@ -46,9 +60,163 @@ export const ComposerInput = ({
}
}, [inputRef]);
+ useEffect(() => {
+ if (
+ value &&
+ //@ts-ignore
+ value.length > 0 &&
+ showTransliteration && transliterationConfig
+ ) {
+ if (suggestionClicked) {
+ setSuggestionClicked(false);
+ return;
+ }
+
+ setSuggestions([]);
+
+ //@ts-ignore
+ const words = value.split(' ');
+ const wordUnderCursor = words.find(
+ (word: any) =>
+ //@ts-ignore
+ cursorPosition >= value.indexOf(word) &&
+ //@ts-ignore
+ cursorPosition <= value.indexOf(word) + word.length,
+ );
+ if (!wordUnderCursor) return;
+ fetch(transliterationConfig.transliterationApi, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ "inputLanguage": transliterationConfig.transliterationInputLanguage,
+ "outputLanguage": transliterationConfig.transliterationOutputLanguage,
+ "input": wordUnderCursor,
+ "provider": transliterationConfig?.transliterationProvider || "bhashini",
+ "numSuggestions": transliterationConfig?.transliterationSuggestions || 3
+ }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ setSuggestions(data?.suggestions);
+ })
+ .catch((error) => {
+ console.error('Error fetching transliteration:', error);
+ });
+ } else {
+ setSuggestions([]);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value, cursorPosition]);
+
+ const suggestionClickHandler = useCallback(
+ (e: any) => {
+ //@ts-ignore
+ const words = value.split(' ');
+
+ // Find the word at the cursor position
+ const selectedWord = words.find(
+ (word: any) =>
+ //@ts-ignore
+ cursorPosition >= value.indexOf(word) &&
+ //@ts-ignore
+ cursorPosition <= value.indexOf(word) + word.length,
+ );
+
+ if (selectedWord) {
+ // Replace the selected word with the transliterated suggestion
+ //@ts-ignore
+ const newInputMsg = value.replace(
+ selectedWord,
+ //@ts-ignore
+ cursorPosition === value.length ? e + ' ' : e,
+ );
+
+ setSuggestions([]);
+ setSuggestionClicked(true);
+ setActiveSuggestion(0);
+
+ // Save and restore the cursor position
+ const restoredCursorPosition =
+ //@ts-ignore
+ cursorPosition - value.indexOf(selectedWord) + value.indexOf(e);
+ //@ts-ignore
+ onChange(newInputMsg, e);
+ setCursorPosition(restoredCursorPosition);
+ //@ts-ignore
+ inputRef.current && inputRef.current.focus();
+ }
+ },
+ [value, cursorPosition, onChange],
+ );
+
+ // @ts-ignore
+ const suggestionHandler = (e: any, index: number) => {
+ setActiveSuggestion(index);
+ };
+
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (suggestions.length > 0) {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setActiveSuggestion((prevActiveSuggestion) =>
+ prevActiveSuggestion > 0 ? prevActiveSuggestion - 1 : prevActiveSuggestion,
+ );
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setActiveSuggestion((prevActiveSuggestion) =>
+ prevActiveSuggestion < suggestions.length - 1
+ ? prevActiveSuggestion + 1
+ : prevActiveSuggestion,
+ );
+ } else if (e.key === ' ') {
+ e.preventDefault();
+ if (activeSuggestion >= 0 && activeSuggestion < suggestions?.length) {
+ suggestionClickHandler(suggestions[activeSuggestion]);
+ setSuggestions([]);
+ } else {
+ //@ts-ignore
+ onChange(prevInputMsg + ' ');
+ }
+ }
+ }
+ },
+ [activeSuggestion, suggestionClickHandler, suggestions],
+ );
+
+ useEffect(() => {
+ if (suggestions.length === 1) {
+ setSuggestions([]);
+ }
+ }, [suggestions]);
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [handleKeyDown]);
+
return (
-
+ {suggestions.map((elem, index) => {
+ return (
+
suggestionClickHandler(elem)}
+ className={`suggestion ${activeSuggestion === index ? 'active' : ''}`}
+ onMouseEnter={(e) => suggestionHandler(e, index)}
+ >
+ {elem}
+
+ );
+ })}
+
+
{pastedImage && (
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index c8463a3..299d19a 100644
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -1,4 +1,10 @@
-import React, { useState, useRef, useEffect, useImperativeHandle, useCallback } from 'react';
+import React, {
+ useState,
+ useRef,
+ useEffect,
+ useImperativeHandle,
+ useCallback,
+} from 'react';
import clsx from 'clsx';
import { IconButtonProps } from '../IconButton';
import { Recorder, RecorderProps } from '../Recorder';
@@ -11,6 +17,7 @@ import { ComposerInput } from './ComposerInput';
import { SendButton } from './SendButton';
import { Action } from './Action';
import toggleClass from '../../utils/toggleClass';
+import { TransliterationConfig } from '../Chat';
export const CLASS_NAME_FOCUSING = 'S--focusing';
@@ -35,6 +42,8 @@ export type ComposerProps = {
onAccessoryToggle?: (isAccessoryOpen: boolean) => void;
rightAction?: IconButtonProps;
disableSend:boolean;
+ showTransliteration: boolean;
+ transliterationConfig:TransliterationConfig | null;
btnColor:string;
voiceToText?:any;
voiceToTextProps?:any;
@@ -59,7 +68,9 @@ export const Composer = React.forwardRef((props,
onSend,
voiceToText: VoiceToText,
voiceToTextProps,
- disableSend=false,
+ disableSend = false,
+ showTransliteration = true,
+ transliterationConfig = null,
onImageSend,
onAccessoryToggle,
toolbar = [],
@@ -69,7 +80,6 @@ export const Composer = React.forwardRef((props,
btnColor,
} = props;
-
const [text, setText] = useState(initialText);
const [textOnce, setTextOnce] = useState('');
const [placeholder, setPlaceholder] = useState(oPlaceholder);
@@ -82,6 +92,7 @@ export const Composer = React.forwardRef((props,
const popoverTarget = useRef();
const isMountRef = useRef(false);
const [isWide, setWide] = useState(false);
+ const [cursorPosition, setCursorPosition] = useState(0);
useEffect(() => {
const mq =
@@ -208,6 +219,9 @@ export const Composer = React.forwardRef((props,
const handleTextChange = useCallback(
(value: string, e: React.ChangeEvent) => {
setText(value);
+ if (e.target instanceof HTMLTextAreaElement) {
+ setCursorPosition(e.target.selectionStart);
+ }
if (onChange) {
onChange(value, e);
@@ -285,10 +299,17 @@ export const Composer = React.forwardRef((props,
)}
);
@@ -296,7 +317,10 @@ export const Composer = React.forwardRef((props,
return (
<>
-
+
{recorder.canRecord && (
((props,
aria-label={isInputText ? 'Switch to voice input' : 'Switch to keyboard input'}
/>
)}
-
-
+
+ {
+
+ }
{!isInputText && }
{!text && rightAction &&
}
-
+
{!text && VoiceToText ? (
) : null}
-
+
{hasToolbar && (
((props,
aria-label={isAccessoryOpen ? 'Close Toolbar' : 'Expand Toolbar'}
/>
)}
- {(text || textOnce ) && }
+ {(text || textOnce) && (
+
+ )}
{isAccessoryOpen && (
@@ -344,4 +379,4 @@ export const Composer = React.forwardRef((props,
)}
>
);
-});
+});
\ No newline at end of file
diff --git a/src/components/Composer/style.less b/src/components/Composer/style.less
index 6a7cabc..848bb1f 100644
--- a/src/components/Composer/style.less
+++ b/src/components/Composer/style.less
@@ -8,6 +8,26 @@
}
}
+.suggestions{
+ position: absolute;
+ display: flex;
+ min-width: 50px;
+ width: auto;
+ bottom: 42px;
+ left: 10px;
+ background-color: white;
+ box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
+}
+
+.suggestion{
+ padding: 0 10px;
+ cursor: pointer;
+}
+.active{
+ background-color: #65c3d7;
+ color: white;
+}
+
.Composer-actions {
display: flex;
align-items: center;