Spaces:
Runtime error
Runtime error
init front
Browse files- front/.bolt/config.json +3 -0
- front/.bolt/prompt +8 -0
- front/.gitignore +24 -0
- front/eslint.config.js +28 -0
- front/index.html +13 -0
- front/package-lock.json +0 -0
- front/package.json +35 -0
- front/postcss.config.js +6 -0
- front/src/App.tsx +39 -0
- front/src/context/ChatContext.tsx +38 -0
- front/src/index.css +3 -0
- front/src/main.tsx +10 -0
- front/src/pages/ArabicChat.tsx +138 -0
- front/src/pages/FrenchChat.tsx +138 -0
- front/src/pages/LanguageSelection.tsx +65 -0
- front/src/vite-env.d.ts +6 -0
- front/tailwind.config.js +8 -0
- front/tsconfig.app.json +24 -0
- front/tsconfig.json +7 -0
- front/tsconfig.node.json +22 -0
- front/vite.config.ts +10 -0
front/.bolt/config.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"template": "bolt-vite-react-ts"
|
3 |
+
}
|
front/.bolt/prompt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
|
2 |
+
|
3 |
+
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
|
4 |
+
|
5 |
+
Use icons from lucide-react for logos.
|
6 |
+
|
7 |
+
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.
|
8 |
+
|
front/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
front/eslint.config.js
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import js from '@eslint/js';
|
2 |
+
import globals from 'globals';
|
3 |
+
import reactHooks from 'eslint-plugin-react-hooks';
|
4 |
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
5 |
+
import tseslint from 'typescript-eslint';
|
6 |
+
|
7 |
+
export default tseslint.config(
|
8 |
+
{ ignores: ['dist'] },
|
9 |
+
{
|
10 |
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
11 |
+
files: ['**/*.{ts,tsx}'],
|
12 |
+
languageOptions: {
|
13 |
+
ecmaVersion: 2020,
|
14 |
+
globals: globals.browser,
|
15 |
+
},
|
16 |
+
plugins: {
|
17 |
+
'react-hooks': reactHooks,
|
18 |
+
'react-refresh': reactRefresh,
|
19 |
+
},
|
20 |
+
rules: {
|
21 |
+
...reactHooks.configs.recommended.rules,
|
22 |
+
'react-refresh/only-export-components': [
|
23 |
+
'warn',
|
24 |
+
{ allowConstantExport: true },
|
25 |
+
],
|
26 |
+
},
|
27 |
+
}
|
28 |
+
);
|
front/index.html
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>MSA translator</title>
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<div id="root"></div>
|
11 |
+
<script type="module" src="/src/main.tsx"></script>
|
12 |
+
</body>
|
13 |
+
</html>
|
front/package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
front/package.json
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "voice-chat-translation-app",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.0",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"lint": "eslint .",
|
10 |
+
"preview": "vite preview"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"@microsoft/fetch-event-source": "^2.0.1",
|
14 |
+
"lucide-react": "^0.344.0",
|
15 |
+
"react": "^18.3.1",
|
16 |
+
"react-dom": "^18.3.1",
|
17 |
+
"react-router-dom": "^6.22.3"
|
18 |
+
},
|
19 |
+
"devDependencies": {
|
20 |
+
"@eslint/js": "^9.9.1",
|
21 |
+
"@types/react": "^18.3.5",
|
22 |
+
"@types/react-dom": "^18.3.0",
|
23 |
+
"@vitejs/plugin-react": "^4.3.1",
|
24 |
+
"autoprefixer": "^10.4.18",
|
25 |
+
"eslint": "^9.9.1",
|
26 |
+
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
27 |
+
"eslint-plugin-react-refresh": "^0.4.11",
|
28 |
+
"globals": "^15.9.0",
|
29 |
+
"postcss": "^8.4.35",
|
30 |
+
"tailwindcss": "^3.4.1",
|
31 |
+
"typescript": "^5.5.3",
|
32 |
+
"typescript-eslint": "^8.3.0",
|
33 |
+
"vite": "^5.4.2"
|
34 |
+
}
|
35 |
+
}
|
front/postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
};
|
front/src/App.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
3 |
+
import { Globe2 } from 'lucide-react';
|
4 |
+
import LanguageSelection from './pages/LanguageSelection';
|
5 |
+
import ArabicChat from './pages/ArabicChat';
|
6 |
+
import FrenchChat from './pages/FrenchChat';
|
7 |
+
import { ChatProvider } from './context/ChatContext';
|
8 |
+
|
9 |
+
function App() {
|
10 |
+
return (
|
11 |
+
<ChatProvider>
|
12 |
+
<Router>
|
13 |
+
<div className="min-h-screen bg-[#F6F6F6]">
|
14 |
+
<nav className="bg-white shadow-md p-4">
|
15 |
+
<div className="container mx-auto flex items-center justify-between">
|
16 |
+
<Link to="/" className="flex items-center space-x-2">
|
17 |
+
<Globe2 className="h-6 w-6 text-blue-600" />
|
18 |
+
<span className="text-xl font-semibold">MSA Translator</span>
|
19 |
+
</Link>
|
20 |
+
<div className="flex space-x-4">
|
21 |
+
<Link to="/" className="px-4 py-2 rounded hover:bg-gray-100">Home</Link>
|
22 |
+
<Link to="/arabic" className="px-4 py-2 rounded hover:bg-gray-100">Arabic Chat</Link>
|
23 |
+
<Link to="/french" className="px-4 py-2 rounded hover:bg-gray-100">French Chat</Link>
|
24 |
+
</div>
|
25 |
+
</div>
|
26 |
+
</nav>
|
27 |
+
|
28 |
+
<Routes>
|
29 |
+
<Route path="/" element={<LanguageSelection />} />
|
30 |
+
<Route path="/arabic" element={<ArabicChat />} />
|
31 |
+
<Route path="/french" element={<FrenchChat />} />
|
32 |
+
</Routes>
|
33 |
+
</div>
|
34 |
+
</Router>
|
35 |
+
</ChatProvider>
|
36 |
+
);
|
37 |
+
}
|
38 |
+
|
39 |
+
export default App;
|
front/src/context/ChatContext.tsx
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { createContext, useContext, useState } from 'react';
|
2 |
+
|
3 |
+
interface Message {
|
4 |
+
id: number;
|
5 |
+
text: string;
|
6 |
+
isArabic?: boolean;
|
7 |
+
isFrench?: boolean;
|
8 |
+
timestamp: Date;
|
9 |
+
}
|
10 |
+
|
11 |
+
interface ChatContextType {
|
12 |
+
messages: Message[];
|
13 |
+
addMessage: (message: Message) => void;
|
14 |
+
}
|
15 |
+
|
16 |
+
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
17 |
+
|
18 |
+
export function ChatProvider({ children }: { children: React.ReactNode }) {
|
19 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
20 |
+
|
21 |
+
const addMessage = (message: Message) => {
|
22 |
+
setMessages(prev => [...prev, message]);
|
23 |
+
};
|
24 |
+
|
25 |
+
return (
|
26 |
+
<ChatContext.Provider value={{ messages, addMessage }}>
|
27 |
+
{children}
|
28 |
+
</ChatContext.Provider>
|
29 |
+
);
|
30 |
+
}
|
31 |
+
|
32 |
+
export function useChat() {
|
33 |
+
const context = useContext(ChatContext);
|
34 |
+
if (context === undefined) {
|
35 |
+
throw new Error('useChat must be used within a ChatProvider');
|
36 |
+
}
|
37 |
+
return context;
|
38 |
+
}
|
front/src/index.css
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
front/src/main.tsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { StrictMode } from 'react';
|
2 |
+
import { createRoot } from 'react-dom/client';
|
3 |
+
import App from './App.tsx';
|
4 |
+
import './index.css';
|
5 |
+
|
6 |
+
createRoot(document.getElementById('root')!).render(
|
7 |
+
<StrictMode>
|
8 |
+
<App />
|
9 |
+
</StrictMode>
|
10 |
+
);
|
front/src/pages/ArabicChat.tsx
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useRef } from 'react';
|
2 |
+
import { Mic, Play, Square } from 'lucide-react';
|
3 |
+
import { useChat } from '../context/ChatContext';
|
4 |
+
|
5 |
+
const ArabicChat = () => {
|
6 |
+
const { messages, addMessage } = useChat();
|
7 |
+
const [isRecording, setIsRecording] = useState(false);
|
8 |
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
9 |
+
const chunksRef = useRef<Blob[]>([]);
|
10 |
+
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
11 |
+
|
12 |
+
const startRecording = async () => {
|
13 |
+
try {
|
14 |
+
// Initialize speech recognition
|
15 |
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
16 |
+
const recognition = new SpeechRecognition();
|
17 |
+
recognitionRef.current = recognition;
|
18 |
+
|
19 |
+
recognition.lang = 'ar-SA';
|
20 |
+
recognition.continuous = true;
|
21 |
+
recognition.interimResults = true;
|
22 |
+
|
23 |
+
recognition.onresult = (event) => {
|
24 |
+
const transcript = Array.from(event.results)
|
25 |
+
.map(result => result[0].transcript)
|
26 |
+
.join('');
|
27 |
+
|
28 |
+
if (event.results[0].isFinal) {
|
29 |
+
const newMessage = {
|
30 |
+
id: Date.now(),
|
31 |
+
text: transcript,
|
32 |
+
isArabic: true,
|
33 |
+
timestamp: new Date(),
|
34 |
+
};
|
35 |
+
addMessage(newMessage);
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
recognition.start();
|
40 |
+
setIsRecording(true);
|
41 |
+
|
42 |
+
// Also start audio recording for WAV file
|
43 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
44 |
+
const mediaRecorder = new MediaRecorder(stream);
|
45 |
+
mediaRecorderRef.current = mediaRecorder;
|
46 |
+
chunksRef.current = [];
|
47 |
+
|
48 |
+
mediaRecorder.ondataavailable = (e) => {
|
49 |
+
chunksRef.current.push(e.data);
|
50 |
+
};
|
51 |
+
|
52 |
+
mediaRecorder.start();
|
53 |
+
} catch (err) {
|
54 |
+
console.error('Error accessing microphone:', err);
|
55 |
+
}
|
56 |
+
};
|
57 |
+
|
58 |
+
const stopRecording = () => {
|
59 |
+
if (recognitionRef.current) {
|
60 |
+
recognitionRef.current.stop();
|
61 |
+
}
|
62 |
+
|
63 |
+
if (mediaRecorderRef.current && isRecording) {
|
64 |
+
mediaRecorderRef.current.stop();
|
65 |
+
setIsRecording(false);
|
66 |
+
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
|
67 |
+
}
|
68 |
+
};
|
69 |
+
|
70 |
+
const speakText = (text: string, isArabic: boolean) => {
|
71 |
+
if ('speechSynthesis' in window) {
|
72 |
+
window.speechSynthesis.cancel();
|
73 |
+
|
74 |
+
const utterance = new SpeechSynthesisUtterance(text);
|
75 |
+
utterance.lang = isArabic ? 'ar-SA' : 'fr-FR';
|
76 |
+
utterance.rate = 1.0;
|
77 |
+
utterance.pitch = 1.0;
|
78 |
+
utterance.volume = 1.0;
|
79 |
+
|
80 |
+
window.speechSynthesis.speak(utterance);
|
81 |
+
}
|
82 |
+
};
|
83 |
+
|
84 |
+
return (
|
85 |
+
<div className="container mx-auto p-4 max-w-2xl">
|
86 |
+
<div className="bg-white rounded-lg shadow-md h-[calc(100vh-12rem)] flex flex-col">
|
87 |
+
<div className="flex-1 overflow-y-auto p-4">
|
88 |
+
{messages.map((message) => (
|
89 |
+
<div
|
90 |
+
key={message.id}
|
91 |
+
className={`flex ${message.isArabic ? 'justify-end' : 'justify-start'} mb-4`}
|
92 |
+
>
|
93 |
+
<div
|
94 |
+
className={`rounded-lg p-3 max-w-[70%] ${
|
95 |
+
message.isArabic
|
96 |
+
? 'bg-blue-500 text-white'
|
97 |
+
: 'bg-white border border-gray-300'
|
98 |
+
}`}
|
99 |
+
>
|
100 |
+
<p dir={message.isArabic ? 'rtl' : 'ltr'}>{message.text}</p>
|
101 |
+
{!message.isArabic && (
|
102 |
+
<button
|
103 |
+
className="mt-2 p-2 bg-gray-100 rounded-full hover:bg-gray-200 transition-colors"
|
104 |
+
onClick={() => speakText(message.text, false)}
|
105 |
+
title="Écouter la traduction"
|
106 |
+
>
|
107 |
+
<Play className="h-4 w-4" />
|
108 |
+
</button>
|
109 |
+
)}
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
))}
|
113 |
+
</div>
|
114 |
+
|
115 |
+
<div className="border-t p-4">
|
116 |
+
<div className="flex items-center justify-center space-x-4">
|
117 |
+
<button
|
118 |
+
onClick={isRecording ? stopRecording : startRecording}
|
119 |
+
className="relative p-4 rounded-full bg-blue-500 hover:bg-blue-600 text-white"
|
120 |
+
>
|
121 |
+
{isRecording ? (
|
122 |
+
<Square className="h-6 w-6" />
|
123 |
+
) : (
|
124 |
+
<Mic className="h-6 w-6" />
|
125 |
+
)}
|
126 |
+
{isRecording && (
|
127 |
+
<div className="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-75"></div>
|
128 |
+
)}
|
129 |
+
</button>
|
130 |
+
<p className="text-gray-600 text-right" dir="rtl">انقر ثم تحدث</p>
|
131 |
+
</div>
|
132 |
+
</div>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
);
|
136 |
+
};
|
137 |
+
|
138 |
+
export default ArabicChat;
|
front/src/pages/FrenchChat.tsx
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useRef } from 'react';
|
2 |
+
import { Mic, Play, Square } from 'lucide-react';
|
3 |
+
import { useChat } from '../context/ChatContext';
|
4 |
+
|
5 |
+
const FrenchChat = () => {
|
6 |
+
const { messages, addMessage } = useChat();
|
7 |
+
const [isRecording, setIsRecording] = useState(false);
|
8 |
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
9 |
+
const chunksRef = useRef<Blob[]>([]);
|
10 |
+
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
11 |
+
|
12 |
+
const startRecording = async () => {
|
13 |
+
try {
|
14 |
+
// Initialize speech recognition
|
15 |
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
16 |
+
const recognition = new SpeechRecognition();
|
17 |
+
recognitionRef.current = recognition;
|
18 |
+
|
19 |
+
recognition.lang = 'fr-FR';
|
20 |
+
recognition.continuous = true;
|
21 |
+
recognition.interimResults = true;
|
22 |
+
|
23 |
+
recognition.onresult = (event) => {
|
24 |
+
const transcript = Array.from(event.results)
|
25 |
+
.map(result => result[0].transcript)
|
26 |
+
.join('');
|
27 |
+
|
28 |
+
if (event.results[0].isFinal) {
|
29 |
+
const newMessage = {
|
30 |
+
id: Date.now(),
|
31 |
+
text: transcript,
|
32 |
+
isFrench: true,
|
33 |
+
timestamp: new Date(),
|
34 |
+
};
|
35 |
+
addMessage(newMessage);
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
recognition.start();
|
40 |
+
setIsRecording(true);
|
41 |
+
|
42 |
+
// Also start audio recording for WAV file
|
43 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
44 |
+
const mediaRecorder = new MediaRecorder(stream);
|
45 |
+
mediaRecorderRef.current = mediaRecorder;
|
46 |
+
chunksRef.current = [];
|
47 |
+
|
48 |
+
mediaRecorder.ondataavailable = (e) => {
|
49 |
+
chunksRef.current.push(e.data);
|
50 |
+
};
|
51 |
+
|
52 |
+
mediaRecorder.start();
|
53 |
+
} catch (err) {
|
54 |
+
console.error('Error accessing microphone:', err);
|
55 |
+
}
|
56 |
+
};
|
57 |
+
|
58 |
+
const stopRecording = () => {
|
59 |
+
if (recognitionRef.current) {
|
60 |
+
recognitionRef.current.stop();
|
61 |
+
}
|
62 |
+
|
63 |
+
if (mediaRecorderRef.current && isRecording) {
|
64 |
+
mediaRecorderRef.current.stop();
|
65 |
+
setIsRecording(false);
|
66 |
+
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
|
67 |
+
}
|
68 |
+
};
|
69 |
+
|
70 |
+
const speakText = (text: string, isFrench: boolean) => {
|
71 |
+
if ('speechSynthesis' in window) {
|
72 |
+
window.speechSynthesis.cancel();
|
73 |
+
|
74 |
+
const utterance = new SpeechSynthesisUtterance(text);
|
75 |
+
utterance.lang = isFrench ? 'fr-FR' : 'ar-SA';
|
76 |
+
utterance.rate = 1.0;
|
77 |
+
utterance.pitch = 1.0;
|
78 |
+
utterance.volume = 1.0;
|
79 |
+
|
80 |
+
window.speechSynthesis.speak(utterance);
|
81 |
+
}
|
82 |
+
};
|
83 |
+
|
84 |
+
return (
|
85 |
+
<div className="container mx-auto p-4 max-w-2xl">
|
86 |
+
<div className="bg-white rounded-lg shadow-md h-[calc(100vh-12rem)] flex flex-col">
|
87 |
+
<div className="flex-1 overflow-y-auto p-4">
|
88 |
+
{messages.map((message) => (
|
89 |
+
<div
|
90 |
+
key={message.id}
|
91 |
+
className={`flex ${message.isFrench ? 'justify-end' : 'justify-start'} mb-4`}
|
92 |
+
>
|
93 |
+
<div
|
94 |
+
className={`rounded-lg p-3 max-w-[70%] ${
|
95 |
+
message.isFrench
|
96 |
+
? 'bg-blue-500 text-white'
|
97 |
+
: 'bg-white border border-gray-300'
|
98 |
+
}`}
|
99 |
+
>
|
100 |
+
<p>{message.text}</p>
|
101 |
+
{!message.isFrench && (
|
102 |
+
<button
|
103 |
+
className="mt-2 p-2 bg-gray-100 rounded-full hover:bg-gray-200 transition-colors"
|
104 |
+
onClick={() => speakText(message.text, false)}
|
105 |
+
title="Écouter le message"
|
106 |
+
>
|
107 |
+
<Play className="h-4 w-4" />
|
108 |
+
</button>
|
109 |
+
)}
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
))}
|
113 |
+
</div>
|
114 |
+
|
115 |
+
<div className="border-t p-4">
|
116 |
+
<div className="flex items-center justify-center space-x-4">
|
117 |
+
<button
|
118 |
+
onClick={isRecording ? stopRecording : startRecording}
|
119 |
+
className="relative p-4 rounded-full bg-blue-500 hover:bg-blue-600 text-white"
|
120 |
+
>
|
121 |
+
{isRecording ? (
|
122 |
+
<Square className="h-6 w-6" />
|
123 |
+
) : (
|
124 |
+
<Mic className="h-6 w-6" />
|
125 |
+
)}
|
126 |
+
{isRecording && (
|
127 |
+
<div className="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-75"></div>
|
128 |
+
)}
|
129 |
+
</button>
|
130 |
+
<p className="text-gray-600">Cliquer puis parler</p>
|
131 |
+
</div>
|
132 |
+
</div>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
);
|
136 |
+
};
|
137 |
+
|
138 |
+
export default FrenchChat;
|
front/src/pages/LanguageSelection.tsx
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { useNavigate } from 'react-router-dom';
|
3 |
+
|
4 |
+
const LanguageSelection = () => {
|
5 |
+
const navigate = useNavigate();
|
6 |
+
|
7 |
+
const languages = [
|
8 |
+
{
|
9 |
+
name: 'Bulgarian',
|
10 |
+
nativeName: 'Говоря български',
|
11 |
+
flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
|
12 |
+
},
|
13 |
+
{
|
14 |
+
name: 'Arabic',
|
15 |
+
nativeName: 'أتحدث العربية',
|
16 |
+
flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
|
17 |
+
},
|
18 |
+
{
|
19 |
+
name: 'Spanish',
|
20 |
+
nativeName: 'Hablo español',
|
21 |
+
flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
|
22 |
+
},
|
23 |
+
{
|
24 |
+
name: 'Romanian',
|
25 |
+
nativeName: 'Vorbesc română',
|
26 |
+
flag: 'https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=64&h=64',
|
27 |
+
},
|
28 |
+
];
|
29 |
+
|
30 |
+
return (
|
31 |
+
<div className="container mx-auto px-4 py-8">
|
32 |
+
<div className="max-w-2xl mx-auto">
|
33 |
+
<div className="flex justify-center mb-8">
|
34 |
+
<img
|
35 |
+
src="https://images.unsplash.com/photo-1607427293702-036707fc51c0?auto=format&fit=crop&w=200&h=100"
|
36 |
+
alt="MSA Logo"
|
37 |
+
className="h-24"
|
38 |
+
/>
|
39 |
+
</div>
|
40 |
+
|
41 |
+
<div className="grid grid-cols-2 gap-4">
|
42 |
+
{languages.map((lang) => (
|
43 |
+
<button
|
44 |
+
key={lang.name}
|
45 |
+
onClick={() => navigate('/arabic')}
|
46 |
+
className="bg-white rounded-lg shadow-md p-4 flex items-center space-x-4 hover:shadow-lg transition-shadow"
|
47 |
+
>
|
48 |
+
<img
|
49 |
+
src={lang.flag}
|
50 |
+
alt={`${lang.name} flag`}
|
51 |
+
className="w-12 h-12 rounded-full"
|
52 |
+
/>
|
53 |
+
<div className="text-left">
|
54 |
+
<p className="text-sm text-gray-600">Je parle</p>
|
55 |
+
<p className="text-lg font-semibold">{lang.nativeName}</p>
|
56 |
+
</div>
|
57 |
+
</button>
|
58 |
+
))}
|
59 |
+
</div>
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
);
|
63 |
+
};
|
64 |
+
|
65 |
+
export default LanguageSelection;
|
front/src/vite-env.d.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/// <reference types="vite/client" />
|
2 |
+
|
3 |
+
interface Window {
|
4 |
+
SpeechRecognition: typeof SpeechRecognition;
|
5 |
+
webkitSpeechRecognition: typeof SpeechRecognition;
|
6 |
+
}
|
front/tailwind.config.js
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('tailwindcss').Config} */
|
2 |
+
export default {
|
3 |
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
4 |
+
theme: {
|
5 |
+
extend: {},
|
6 |
+
},
|
7 |
+
plugins: [],
|
8 |
+
};
|
front/tsconfig.app.json
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "ES2020",
|
4 |
+
"useDefineForClassFields": true,
|
5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
6 |
+
"module": "ESNext",
|
7 |
+
"skipLibCheck": true,
|
8 |
+
|
9 |
+
/* Bundler mode */
|
10 |
+
"moduleResolution": "bundler",
|
11 |
+
"allowImportingTsExtensions": true,
|
12 |
+
"isolatedModules": true,
|
13 |
+
"moduleDetection": "force",
|
14 |
+
"noEmit": true,
|
15 |
+
"jsx": "react-jsx",
|
16 |
+
|
17 |
+
/* Linting */
|
18 |
+
"strict": true,
|
19 |
+
"noUnusedLocals": true,
|
20 |
+
"noUnusedParameters": true,
|
21 |
+
"noFallthroughCasesInSwitch": true
|
22 |
+
},
|
23 |
+
"include": ["src"]
|
24 |
+
}
|
front/tsconfig.json
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"files": [],
|
3 |
+
"references": [
|
4 |
+
{ "path": "./tsconfig.app.json" },
|
5 |
+
{ "path": "./tsconfig.node.json" }
|
6 |
+
]
|
7 |
+
}
|
front/tsconfig.node.json
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "ES2022",
|
4 |
+
"lib": ["ES2023"],
|
5 |
+
"module": "ESNext",
|
6 |
+
"skipLibCheck": true,
|
7 |
+
|
8 |
+
/* Bundler mode */
|
9 |
+
"moduleResolution": "bundler",
|
10 |
+
"allowImportingTsExtensions": true,
|
11 |
+
"isolatedModules": true,
|
12 |
+
"moduleDetection": "force",
|
13 |
+
"noEmit": true,
|
14 |
+
|
15 |
+
/* Linting */
|
16 |
+
"strict": true,
|
17 |
+
"noUnusedLocals": true,
|
18 |
+
"noUnusedParameters": true,
|
19 |
+
"noFallthroughCasesInSwitch": true
|
20 |
+
},
|
21 |
+
"include": ["vite.config.ts"]
|
22 |
+
}
|
front/vite.config.ts
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'vite';
|
2 |
+
import react from '@vitejs/plugin-react';
|
3 |
+
|
4 |
+
// https://vitejs.dev/config/
|
5 |
+
export default defineConfig({
|
6 |
+
plugins: [react()],
|
7 |
+
optimizeDeps: {
|
8 |
+
exclude: ['lucide-react'],
|
9 |
+
},
|
10 |
+
});
|