Compare commits

...

4 Commits

Author SHA1 Message Date
fred-bf
52316785d1 Merge branch 'main' into feat/voice-input 2024-03-19 17:52:00 +08:00
Fred
e2b15f785a feat: update voice input button 2024-03-18 17:53:59 +08:00
Fred
42d04d473e fix: update ui 2024-03-13 23:06:35 +08:00
Fred
2f53107581 feat: init voice support 2024-03-04 20:04:19 +08:00
11 changed files with 1387 additions and 884 deletions

View File

@@ -58,7 +58,7 @@
box-shadow: var(--card-shadow);
transition: width ease 0.3s;
align-items: center;
height: 16px;
height: 24px;
width: var(--icon-width);
overflow: hidden;
@@ -68,7 +68,6 @@
.text {
white-space: nowrap;
padding-left: 5px;
opacity: 0;
transform: translateX(-5px);
transition: all ease 0.3s;
@@ -610,10 +609,6 @@
.chat-input-send {
background-color: var(--primary);
color: white;
position: absolute;
right: 30px;
bottom: 32px;
}
@media only screen and (max-width: 600px) {

View File

@@ -97,7 +97,7 @@ import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks";
import { MultimodalContent } from "../client/api";
import SpeechRecorder from "./chat/speechRecorder";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
});
@@ -347,7 +347,7 @@ function ChatAction(props: {
full: 16,
icon: 16,
});
const [isActive, setIsActive] = useState(false);
function updateWidth() {
if (!iconRef.current || !textRef.current) return;
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
@@ -361,25 +361,22 @@ function ChatAction(props: {
return (
<div
className={`${styles["chat-input-action"]} clickable`}
className={`${styles["chat-input-action"]} clickable group`}
onClick={() => {
props.onClick();
setTimeout(updateWidth, 1);
}}
onMouseEnter={updateWidth}
onTouchStart={updateWidth}
style={
{
"--icon-width": `${width.icon}px`,
"--full-width": `${width.full}px`,
} as React.CSSProperties
}
>
<div ref={iconRef} className={styles["icon"]}>
{props.icon}
</div>
<div className={styles["text"]} ref={textRef}>
{props.text}
<div className="flex">
<div ref={iconRef} className={styles["icon"]}>
{props.icon}
</div>
<div
className={`${styles["text"]} transition-all duration-1000 w-0 group-hover:w-[60px]`}
ref={textRef}
>
{props.text}
</div>
</div>
</div>
);
@@ -424,6 +421,7 @@ export function ChatActions(props: {
showPromptModal: () => void;
scrollToBottom: () => void;
showPromptHints: () => void;
setUserInput: (text: string) => void;
hitBottom: boolean;
uploading: boolean;
}) {
@@ -1462,6 +1460,7 @@ function _Chat() {
scrollToBottom={scrollToBottom}
hitBottom={hitBottom}
uploading={uploading}
setUserInput={setUserInput}
showPromptHints={() => {
// Click again to close
if (promptHints.length > 0) {
@@ -1522,13 +1521,19 @@ function _Chat() {
})}
</div>
)}
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}
className={styles["chat-input-send"]}
type="primary"
onClick={() => doSubmit(userInput)}
/>
<div className="flex gap-2 absolute left-[30px] bottom-[32px]">
<SpeechRecorder textUpdater={setUserInput}></SpeechRecorder>
</div>
<div className="flex gap-2 absolute right-[30px] bottom-[32px]">
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}
className={styles["chat-input-send"]}
type="primary"
onClick={() => doSubmit(userInput)}
/>
</div>
</label>
</div>

View File

@@ -0,0 +1,64 @@
import React, { useState, useEffect } from "react";
import VoiceIcon from "@/app/icons/voice.svg";
import { getLang, formatLang } from "@/app/locales";
type SpeechRecognitionType =
| typeof window.SpeechRecognition
| typeof window.webkitSpeechRecognition;
export default function SpeechRecorder({
textUpdater,
onStop,
}: {
textUpdater: (text: string) => void;
onStop?: () => void;
}) {
const [speechRecognition, setSpeechRecognition] =
useState<SpeechRecognitionType | null>(null);
const [isRecording, setIsRecording] = useState(false);
useEffect(() => {
if ("SpeechRecognition" in window) {
setSpeechRecognition(new (window as any).SpeechRecognition());
} else if ("webkitSpeechRecognition" in window) {
setSpeechRecognition(new (window as any).webkitSpeechRecognition());
}
}, []);
return (
<>
{speechRecognition && (
<div>
<button
onClick={() => {
if (!isRecording && speechRecognition) {
speechRecognition.continuous = true;
speechRecognition.lang = formatLang(getLang());
console.log(speechRecognition.lang);
speechRecognition.interimResults = true;
speechRecognition.start();
speechRecognition.onresult = function (event: any) {
console.log(event);
var transcript = event.results[0][0].transcript;
console.log(transcript);
textUpdater(transcript);
};
setIsRecording(true);
} else {
speechRecognition.stop();
setIsRecording(false);
}
}}
>
{isRecording ? (
<button className="p-2 rounded-full bg-blue-500 hover:bg-blue-600 ring-4 ring-blue-200 transition animate-pulse">
<VoiceIcon fill={"white"} />
</button>
) : (
<button className="p-2 rounded-full bg-zinc-100 hover:bg-zinc-200 transition">
<VoiceIcon fill={"#8282A5"} />
</button>
)}
</button>
</div>
)}
</>
);
}

11
app/icons/voice.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5.9375 5.3125C5.9375 3.06884 7.75634 1.25 10 1.25C12.2437 1.25 14.0625 3.06884 14.0625 5.3125V8.75C14.0625 10.9937 12.2437 12.8125 10 12.8125C7.75634 12.8125 5.9375 10.9937 5.9375 8.75V5.3125Z"
fill="auto" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M3.35938 7.8125C3.79085 7.8125 4.14062 8.16228 4.14062 8.59375V8.75C4.14062 11.986 6.76396 14.6094 10 14.6094C13.236 14.6094 15.8594 11.986 15.8594 8.75V8.59375C15.8594 8.16228 16.2092 7.8125 16.6406 7.8125C17.0721 7.8125 17.4219 8.16228 17.4219 8.59375V8.75C17.4219 12.849 14.099 16.1719 10 16.1719C5.90101 16.1719 2.57812 12.849 2.57812 8.75V8.59375C2.57812 8.16228 2.9279 7.8125 3.35938 7.8125Z"
fill="auto" />
<path
d="M9.21875 15.4688C9.21875 15.0373 9.56853 14.6875 10 14.6875C10.4315 14.6875 10.7812 15.0373 10.7812 15.4688V17.9688C10.7812 18.4002 10.4315 18.75 10 18.75C9.56853 18.75 9.21875 18.4002 9.21875 17.9688V15.4688Z"
fill="auto" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -70,6 +70,28 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
sk: "Slovensky",
};
const LANG_CODE_MAPPING = {
cn: "zh-CN",
en: "en-US",
tw: "zh-TW",
pt: "pt-BR",
jp: "ja-JP",
ko: "ko-KR",
id: "id-ID",
fr: "fr-FR",
es: "es-ES",
it: "it-IT",
tr: "tr-TR",
de: "de-DE",
vi: "vi-VN",
ru: "ru-RU",
cs: "cs-CZ",
no: "nb-NO",
ar: "ar-SA",
bn: "bn-BD",
sk: "sk-SK",
};
const LANG_KEY = "lang";
const DEFAULT_LANG = "en";
@@ -81,6 +103,13 @@ merge(fallbackLang, targetLang);
export default fallbackLang as LocaleType;
export const formatLang = (languageCode: string) => {
return (
LANG_CODE_MAPPING[languageCode as keyof typeof LANG_CODE_MAPPING] ||
languageCode
);
};
function getItem(key: string) {
try {
return localStorage.getItem(key);

View File

@@ -1,3 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./animation.scss";
@import "./window.scss";

View File

@@ -50,6 +50,7 @@
"@types/react-dom": "^18.2.7",
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.4",
"autoprefixer": "^10.4.18",
"cross-env": "^7.0.3",
"eslint": "^8.49.0",
"eslint-config-next": "13.4.19",
@@ -57,12 +58,14 @@
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.0",
"lint-staged": "^13.2.2",
"postcss": "^8.4.35",
"prettier": "^3.0.2",
"tailwindcss": "^3.4.1",
"typescript": "5.2.2",
"webpack": "^5.88.1"
},
"resolutions": {
"lint-staged/yaml": "^2.2.2"
},
}
"packageManager": "yarn@1.22.19"
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

15
tailwind.config.js Normal file
View File

@@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -23,6 +23,12 @@
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"app/calcTextareaHeight.ts"
],
"exclude": ["node_modules"]
}

2072
yarn.lock

File diff suppressed because it is too large Load Diff