chat utils
This commit is contained in:
		
							parent
							
								
									752b72e2c9
								
							
						
					
					
						commit
						5e76493da2
					
				
							
								
								
									
										12
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -11,6 +11,7 @@
 | 
			
		||||
        "@fluentui/react-components": "^9.20.0",
 | 
			
		||||
        "@fluentui/react-icons": "^2.0.201",
 | 
			
		||||
        "@microsoft/fetch-event-source": "^2.0.1",
 | 
			
		||||
        "@primer/octicons-react": "^19.1.0",
 | 
			
		||||
        "classnames": "^2.3.2",
 | 
			
		||||
        "github-markdown-css": "^5.2.0",
 | 
			
		||||
        "i18next": "^22.4.15",
 | 
			
		||||
@ -1936,6 +1937,17 @@
 | 
			
		||||
        "node": ">= 8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@primer/octicons-react": {
 | 
			
		||||
      "version": "19.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/@primer/octicons-react/-/octicons-react-19.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-owWS3jHcsMOQMRzXURSnbqkkQCgmNOZWmm/vejzwnPU21m8Wz1Xng5i0pu1B/VuW7cmsNh5+r5XsVM8r1igY6A==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": ">=16.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@remix-run/router": {
 | 
			
		||||
      "version": "1.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.6.1.tgz",
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@
 | 
			
		||||
    "@fluentui/react-components": "^9.20.0",
 | 
			
		||||
    "@fluentui/react-icons": "^2.0.201",
 | 
			
		||||
    "@microsoft/fetch-event-source": "^2.0.1",
 | 
			
		||||
    "@primer/octicons-react": "^19.1.0",
 | 
			
		||||
    "classnames": "^2.3.2",
 | 
			
		||||
    "github-markdown-css": "^5.2.0",
 | 
			
		||||
    "i18next": "^22.4.15",
 | 
			
		||||
 | 
			
		||||
@ -66,5 +66,7 @@
 | 
			
		||||
  "Model Status": "模型状态",
 | 
			
		||||
  "Clear": "清除",
 | 
			
		||||
  "Send": "发送",
 | 
			
		||||
  "Type your message here": "在此输入消息"
 | 
			
		||||
  "Type your message here": "在此输入消息",
 | 
			
		||||
  "Copy": "复制",
 | 
			
		||||
  "Read Aloud": "朗读"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								frontend/src/components/CopyButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/components/CopyButton.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
import {FC, useState} from 'react';
 | 
			
		||||
import {CheckIcon, CopyIcon} from '@primer/octicons-react';
 | 
			
		||||
import {useTranslation} from 'react-i18next';
 | 
			
		||||
import {ClipboardSetText} from '../../wailsjs/runtime';
 | 
			
		||||
import {ToolTipButton} from './ToolTipButton';
 | 
			
		||||
 | 
			
		||||
export const CopyButton: FC<{ content: string }> = ({content}) => {
 | 
			
		||||
  const {t} = useTranslation();
 | 
			
		||||
  const [copied, setCopied] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const onClick = () => {
 | 
			
		||||
    ClipboardSetText(content)
 | 
			
		||||
      .then(() => setCopied(true))
 | 
			
		||||
      .then(() =>
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          setCopied(false);
 | 
			
		||||
        }, 600)
 | 
			
		||||
      );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolTipButton desc={t('Copy')} size="small" appearance="subtle" icon={copied ? <CheckIcon/> : <CopyIcon/>}
 | 
			
		||||
                   onClick={onClick}/>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										52
									
								
								frontend/src/components/ReadButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								frontend/src/components/ReadButton.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
import {FC, useState} from 'react';
 | 
			
		||||
import {MuteIcon, UnmuteIcon} from '@primer/octicons-react';
 | 
			
		||||
import {useTranslation} from 'react-i18next';
 | 
			
		||||
import {ToolTipButton} from './ToolTipButton';
 | 
			
		||||
import commonStore from '../stores/commonStore';
 | 
			
		||||
import {observer} from 'mobx-react-lite';
 | 
			
		||||
 | 
			
		||||
const synth = window.speechSynthesis;
 | 
			
		||||
 | 
			
		||||
export const ReadButton: FC<{ content: string }> = observer(({content}) => {
 | 
			
		||||
  const {t} = useTranslation();
 | 
			
		||||
  const [speaking, setSpeaking] = useState(false);
 | 
			
		||||
  let lang: string = commonStore.settings.language;
 | 
			
		||||
  if (lang === 'dev')
 | 
			
		||||
    lang = 'en';
 | 
			
		||||
 | 
			
		||||
  const startSpeak = () => {
 | 
			
		||||
    synth.cancel();
 | 
			
		||||
 | 
			
		||||
    const utterance = new SpeechSynthesisUtterance(content);
 | 
			
		||||
    const voices = synth.getVoices();
 | 
			
		||||
 | 
			
		||||
    let voice;
 | 
			
		||||
    if (lang === 'en')
 | 
			
		||||
      voice = voices.find((v) => v.name.toLowerCase().includes('microsoft aria'));
 | 
			
		||||
    else if (lang === 'zh')
 | 
			
		||||
      voice = voices.find((v) => v.name.toLowerCase().includes('xiaoyi'));
 | 
			
		||||
    if (!voice) voice = voices.find((v) => v.lang.substring(0, 2) === lang);
 | 
			
		||||
    if (!voice) voice = voices.find((v) => v.lang === navigator.language);
 | 
			
		||||
 | 
			
		||||
    Object.assign(utterance, {
 | 
			
		||||
      rate: 1,
 | 
			
		||||
      volume: 1,
 | 
			
		||||
      onend: () => setSpeaking(false),
 | 
			
		||||
      onerror: () => setSpeaking(false),
 | 
			
		||||
      voice: voice
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    synth.speak(utterance);
 | 
			
		||||
    setSpeaking(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const stopSpeak = () => {
 | 
			
		||||
    synth.cancel();
 | 
			
		||||
    setSpeaking(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolTipButton desc={t('Read Aloud')} size="small" appearance="subtle" icon={speaking ? <MuteIcon/> : <UnmuteIcon/>}
 | 
			
		||||
                   onClick={speaking ? stopSpeak : startSpeak}/>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
@ -14,6 +14,8 @@ import logo from '../../../build/appicon.png';
 | 
			
		||||
import MarkdownRender from '../components/MarkdownRender';
 | 
			
		||||
import {ToolTipButton} from '../components/ToolTipButton';
 | 
			
		||||
import {ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular} from '@fluentui/react-icons';
 | 
			
		||||
import {CopyButton} from '../components/CopyButton';
 | 
			
		||||
import {ReadButton} from '../components/ReadButton';
 | 
			
		||||
 | 
			
		||||
const userName = 'M E';
 | 
			
		||||
const botName = 'A I';
 | 
			
		||||
@ -189,6 +191,14 @@ const ChatPanel: FC = observer(() => {
 | 
			
		||||
              'flex gap-2 mb-2 overflow-hidden',
 | 
			
		||||
              conversation.side === 'left' ? 'flex-row' : 'flex-row-reverse'
 | 
			
		||||
            )}
 | 
			
		||||
            onMouseEnter={() => {
 | 
			
		||||
              const utils = document.getElementById('utils-' + uuid);
 | 
			
		||||
              if (utils) utils.classList.remove('invisible');
 | 
			
		||||
            }}
 | 
			
		||||
            onMouseLeave={() => {
 | 
			
		||||
              const utils = document.getElementById('utils-' + uuid);
 | 
			
		||||
              if (utils) utils.classList.add('invisible');
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Avatar
 | 
			
		||||
              color={conversation.color}
 | 
			
		||||
@ -204,11 +214,18 @@ const ChatPanel: FC = observer(() => {
 | 
			
		||||
            >
 | 
			
		||||
              <MarkdownRender>{conversation.content}</MarkdownRender>
 | 
			
		||||
            </div>
 | 
			
		||||
            {(conversation.type === MessageType.Error || !conversation.done) &&
 | 
			
		||||
              <PresenceBadge status={
 | 
			
		||||
                conversation.type === MessageType.Error ? 'busy' : 'away'
 | 
			
		||||
              }/>
 | 
			
		||||
            }
 | 
			
		||||
            <div className="flex flex-col gap-1 items-start">
 | 
			
		||||
              <div className="grow"/>
 | 
			
		||||
              {(conversation.type === MessageType.Error || !conversation.done) &&
 | 
			
		||||
                <PresenceBadge size="extra-small" status={
 | 
			
		||||
                  conversation.type === MessageType.Error ? 'busy' : 'away'
 | 
			
		||||
                }/>
 | 
			
		||||
              }
 | 
			
		||||
              <div className="flex invisible" id={'utils-' + uuid}>
 | 
			
		||||
                <ReadButton content={conversation.content}/>
 | 
			
		||||
                <CopyButton content={conversation.content}/>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>;
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user