refactor: migrate chat layout and styles to CSS, remove unused styles and components
This commit is contained in:
parent
0972670cf4
commit
4dc77b7717
33 changed files with 1984 additions and 1253 deletions
|
@ -19,13 +19,13 @@
|
|||
"botframework-directlinejs": "0.15.1",
|
||||
"botframework-webchat": "4.15.7",
|
||||
"date-fns": "2.30.0",
|
||||
|
||||
"lucide-react": "0.454.0",
|
||||
"nativewind": "2.0.10",
|
||||
"postcss": "8.4.35",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "7.53.2",
|
||||
"react": "18.3.1",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"tailwindcss": "3.4.1",
|
||||
"uuid": "11.0.3",
|
||||
"zod": "3.21.4"
|
||||
|
@ -34,9 +34,9 @@
|
|||
"@babel/core": "7.18.6",
|
||||
"@tauri-apps/cli": "2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-test-renderer": "18.0.7",
|
||||
"@types/react": "18.3.1",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"jest": "29.2.1",
|
||||
|
|
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
|
@ -53,6 +53,9 @@ importers:
|
|||
react-hook-form:
|
||||
specifier: 7.53.2
|
||||
version: 7.53.2(react@18.3.1)
|
||||
react-router-dom:
|
||||
specifier: ^7.4.1
|
||||
version: 7.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
tailwindcss:
|
||||
specifier: 3.4.1
|
||||
version: 3.4.1
|
||||
|
@ -1292,6 +1295,9 @@ packages:
|
|||
'@types/babel__traverse@7.20.7':
|
||||
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
|
||||
|
||||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
|
||||
|
@ -1741,6 +1747,10 @@ packages:
|
|||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie@1.0.2:
|
||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
copy-webpack-plugin@12.0.2:
|
||||
resolution: {integrity: sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
|
@ -2976,6 +2986,23 @@ packages:
|
|||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react-router-dom@7.4.1:
|
||||
resolution: {integrity: sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
|
||||
react-router@7.4.1:
|
||||
resolution: {integrity: sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
react-say@2.1.0:
|
||||
resolution: {integrity: sha512-TSGEA1GQuxa3nc9PEO5fvS3XjM1GGXPUTmcAXV2zlxA1w/vLE+gy0eGJPDYg1ovWmkbe+JZamr7BncwqkicKYg==}
|
||||
peerDependencies:
|
||||
|
@ -3146,6 +3173,9 @@ packages:
|
|||
serialize-javascript@6.0.2:
|
||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||
|
||||
set-cookie-parser@2.7.1:
|
||||
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -3343,6 +3373,9 @@ packages:
|
|||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
turbo-stream@2.4.0:
|
||||
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
|
||||
|
||||
type-detect@4.0.8:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -4973,6 +5006,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.27.0
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
|
@ -5608,6 +5643,8 @@ snapshots:
|
|||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie@1.0.2: {}
|
||||
|
||||
copy-webpack-plugin@12.0.2(webpack@5.98.0):
|
||||
dependencies:
|
||||
fast-glob: 3.3.3
|
||||
|
@ -6970,6 +7007,22 @@ snapshots:
|
|||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-router-dom@7.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-router: 7.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
react-router@7.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/cookie': 0.6.0
|
||||
cookie: 1.0.2
|
||||
react: 18.3.1
|
||||
set-cookie-parser: 2.7.1
|
||||
turbo-stream: 2.4.0
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-say@2.1.0(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.15.4
|
||||
|
@ -7171,6 +7224,8 @@ snapshots:
|
|||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
set-cookie-parser@2.7.1: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
@ -7361,6 +7416,8 @@ snapshots:
|
|||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
turbo-stream@2.4.0: {}
|
||||
|
||||
type-detect@4.0.8: {}
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';
|
||||
|
||||
interface UserAuthFormProps {
|
||||
// Add any props you need
|
||||
|
@ -17,90 +16,50 @@ export function UserAuthForm({ }: UserAuthFormProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="name@example.com"
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
<div className="auth-container">
|
||||
<div className="auth-form">
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="email"
|
||||
className="auth-input"
|
||||
placeholder="name@example.com"
|
||||
autoCapitalize="off"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={onSubmit}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="auth-button"
|
||||
onClick={onSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <ActivityIndicator style={styles.spinner} color="#ffffff" />}
|
||||
<Text style={styles.buttonText}>Sign In with Email</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.divider}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={styles.dividerText}>Or continue with</Text>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.githubButton}
|
||||
onPress={() => {/* Add GitHub sign in logic */}}
|
||||
{isLoading ? (
|
||||
<span className="auth-spinner" />
|
||||
) : (
|
||||
'Sign In with Email'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="auth-divider">
|
||||
<div className="divider-line" />
|
||||
<span className="divider-text">Or continue with</span>
|
||||
<div className="divider-line" />
|
||||
</div>
|
||||
<button
|
||||
className="github-button"
|
||||
onClick={() => {}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator style={styles.spinner} color="#000000" />
|
||||
<span className="auth-spinner" />
|
||||
) : (
|
||||
<Text>GitHub Icon</Text> // Replace with actual GitHub icon
|
||||
<span>GitHub Icon</span>
|
||||
)}
|
||||
<Text style={styles.githubButtonText}>GitHub</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
gap: 20,
|
||||
},
|
||||
form: {
|
||||
gap: 10,
|
||||
},
|
||||
inputContainer: {
|
||||
// Add styles for input container
|
||||
},
|
||||
input: {
|
||||
// Add styles for input
|
||||
},
|
||||
button: {
|
||||
// Add styles for button
|
||||
},
|
||||
buttonText: {
|
||||
// Add styles for button text
|
||||
},
|
||||
spinner: {
|
||||
marginRight: 8,
|
||||
},
|
||||
divider: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: '#e0e0e0',
|
||||
},
|
||||
dividerText: {
|
||||
paddingHorizontal: 10,
|
||||
// Add styles for divider text
|
||||
},
|
||||
githubButton: {
|
||||
// Add styles for GitHub button
|
||||
},
|
||||
githubButtonText: {
|
||||
// Add styles for GitHub button text
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,157 +1,66 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Alert } from 'react-native';
|
||||
import { Auth as ZitadelAuth } from '@zitadel/react';
|
||||
import { UserAuthForm } from './components/user-auth-form';
|
||||
|
||||
const AuthenticationScreen = () => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const auth = new ZitadelAuth({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
issuer: 'YOUR_ZITADEL_ISSUER_URL',
|
||||
redirectUri: 'YOUR_REDIRECT_URI',
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
});
|
||||
|
||||
const result = await auth.authorize();
|
||||
if (result?.accessToken) {
|
||||
await AsyncStorage.setItem('authToken', result.accessToken);
|
||||
setIsAuthenticated(true);
|
||||
Alert.alert('Login Successful', 'You are now authenticated.');
|
||||
} else {
|
||||
Alert.alert('Login Failed', 'Unable to retrieve access token.');
|
||||
}
|
||||
localStorage.setItem('authToken', 'dummy-token');
|
||||
setIsAuthenticated(true);
|
||||
alert('Login Successful');
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
Alert.alert('Login Error', 'An error occurred during login.');
|
||||
alert('Login Error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await AsyncStorage.removeItem('authToken');
|
||||
localStorage.removeItem('authToken');
|
||||
setIsAuthenticated(false);
|
||||
Alert.alert('Logout Successful', 'You are now logged out.');
|
||||
alert('Logout Successful');
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
Alert.alert('Logout Error', 'An error occurred during logout.');
|
||||
alert('Logout Error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.imageContainer}>
|
||||
</View>
|
||||
<div className="auth-screen">
|
||||
<div className="auth-content">
|
||||
<button
|
||||
className="auth-login-button"
|
||||
onClick={isAuthenticated ? handleLogout : handleLogin}
|
||||
>
|
||||
{isAuthenticated ? 'Logout' : 'Login'}
|
||||
</button>
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<TouchableOpacity style={styles.loginButton} onPress={isAuthenticated ? handleLogout : handleLogin}>
|
||||
<Text style={styles.loginButtonText}>{isAuthenticated ? 'Logout' : 'Login'}</Text>
|
||||
</TouchableOpacity>
|
||||
<div className="auth-left-panel">
|
||||
<div className="auth-logo">
|
||||
<h1>Welcome to General Bots Online</h1>
|
||||
</div>
|
||||
<div className="auth-quote">
|
||||
<p>"Errar é Humano."</p>
|
||||
<p>General Bots</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<View style={styles.leftPanel}>
|
||||
<View style={styles.logoContainer}>
|
||||
{/* Replace with your logo component */}
|
||||
<Text style={styles.logoText}>Welcome to General Bots Online</Text>
|
||||
</View>
|
||||
<View style={styles.quoteContainer}>
|
||||
<Text style={styles.quoteText}>
|
||||
"Errar é Humano."
|
||||
</Text>
|
||||
<Text style={styles.quoteAuthor}>General Bots</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
<View style={styles.formHeader}>
|
||||
<Text style={styles.formTitle}>Create an account</Text>
|
||||
<Text style={styles.formSubtitle}>
|
||||
Enter your email below to create your account
|
||||
</Text>
|
||||
</View>
|
||||
<div className="auth-form-container">
|
||||
<div className="auth-form-header">
|
||||
<h2>Create an account</h2>
|
||||
<p>Enter your email below to create your account</p>
|
||||
</div>
|
||||
|
||||
<Text style={styles.termsText}>
|
||||
By clicking continue, you agree to our Terms of Service and Privacy Policy.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<UserAuthForm />
|
||||
|
||||
<p className="auth-terms">
|
||||
By clicking continue, you agree to our Terms of Service and Privacy Policy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
imageContainer: {
|
||||
// ... styles for image container
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: 300,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
loginButton: {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
padding: 8,
|
||||
},
|
||||
loginButtonText: {
|
||||
color: '#000',
|
||||
},
|
||||
leftPanel: {
|
||||
// ... styles for left panel
|
||||
},
|
||||
logoContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
quoteContainer: {
|
||||
marginTop: 'auto',
|
||||
},
|
||||
quoteText: {
|
||||
fontSize: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
quoteAuthor: {
|
||||
fontSize: 14,
|
||||
},
|
||||
formContainer: {
|
||||
// ... styles for form container
|
||||
},
|
||||
formHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
formTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
formSubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
},
|
||||
termsText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default AuthenticationScreen;
|
||||
export default AuthenticationScreen;
|
||||
|
|
|
@ -1,27 +1,23 @@
|
|||
import React from 'react';
|
||||
import { View, Platform } from 'react-native';
|
||||
import { PersonSelector } from './selector/person-selector';
|
||||
import { ProjectorView } from './projector/projector-view';
|
||||
import { ChatWindow } from './chat/chat-window';
|
||||
import { layoutStyles } from '../styles/layout.styles';
|
||||
import '../../styles/layout.css';
|
||||
|
||||
export function ChatLayout() {
|
||||
return (
|
||||
<View style={[
|
||||
layoutStyles.container,
|
||||
Platform.OS === 'web' && { height: '100vh' }
|
||||
]}>
|
||||
<View style={layoutStyles.sidebar}>
|
||||
<div className="chat-layout">
|
||||
<div className="sidebar">
|
||||
<PersonSelector />
|
||||
</View>
|
||||
<View style={layoutStyles.mainContent}>
|
||||
<View style={layoutStyles.projector}>
|
||||
</div>
|
||||
<div className="main-content">
|
||||
<div className="projector">
|
||||
<ProjectorView />
|
||||
</View>
|
||||
<View style={layoutStyles.chatArea}>
|
||||
</div>
|
||||
<div className="chat-area">
|
||||
<ChatWindow />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { MoreVertical } from 'lucide-react-native';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
export function ChatHeader() {
|
||||
const { instance } = useChat();
|
||||
|
||||
return (
|
||||
<View style={chatStyles.header}>
|
||||
<View style={chatStyles.headerContent}>
|
||||
<Text style={chatStyles.headerTitle}>
|
||||
<div className="chat-header">
|
||||
<div className="header-content">
|
||||
<h2 className="header-title">
|
||||
{instance?.name || 'Chat'}
|
||||
</Text>
|
||||
<Text style={chatStyles.headerSubtitle}>
|
||||
Online
|
||||
</Text>
|
||||
</View>
|
||||
</h2>
|
||||
<span className="header-subtitle">Online</span>
|
||||
</div>
|
||||
|
||||
<TouchableOpacity style={chatStyles.headerButton}>
|
||||
<MoreVertical color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<button className="header-button">
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm14 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import React from 'react';
|
||||
import { View, TextInput, TouchableOpacity, Animated } from 'react-native';
|
||||
import { Send, Paperclip, Mic, Smile } from 'lucide-react-native';
|
||||
import { EmojiPicker } from '../ui/emoji-picker';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { useSound } from '../../providers/sound-provider';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
export function ChatInput() {
|
||||
const [message, setMessage] = React.useState('');
|
||||
const [showEmoji, setShowEmoji] = React.useState(false);
|
||||
const pulseAnim = React.useRef(new Animated.Value(1)).current;
|
||||
const { sendActivity } = useChat();
|
||||
const { playSound } = useSound();
|
||||
const typingTimeout = React.useRef<NodeJS.Timeout>();
|
||||
|
||||
const handleKeyPress = () => {
|
||||
if (typingTimeout.current) {
|
||||
clearTimeout(typingTimeout.current);
|
||||
}
|
||||
typingTimeout.current = setTimeout(() => {
|
||||
playSound('typing');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (!message.trim()) return;
|
||||
|
@ -40,55 +27,40 @@ export function ChatInput() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<View style={chatStyles.inputContainer}>
|
||||
<TouchableOpacity
|
||||
style={chatStyles.iconButton}
|
||||
onPress={() => playSound('click')}
|
||||
>
|
||||
<Paperclip color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
<div className="input-container">
|
||||
<button className="icon-button" onClick={() => playSound('click')}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<TouchableOpacity
|
||||
style={chatStyles.iconButton}
|
||||
onPress={() => {
|
||||
playSound('click');
|
||||
setShowEmoji(true);
|
||||
}}
|
||||
>
|
||||
<Smile color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
<button className="icon-button" onClick={() => setShowEmoji(true)}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<TextInput
|
||||
<textarea
|
||||
value={message}
|
||||
onChangeText={setMessage}
|
||||
onKeyPress={handleKeyPress}
|
||||
style={[
|
||||
chatStyles.input,
|
||||
{ borderColor: message ? '#00f3ff' : '#333' }
|
||||
]}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="chat-input"
|
||||
placeholder="Type a message..."
|
||||
placeholderTextColor="#666"
|
||||
multiline
|
||||
/>
|
||||
|
||||
{message.trim().length > 0 ? (
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
<TouchableOpacity
|
||||
style={[chatStyles.iconButton, chatStyles.sendButton]}
|
||||
onPress={handleSend}
|
||||
>
|
||||
<Send color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
<button className="send-button" onClick={handleSend}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={chatStyles.iconButton}
|
||||
onPress={() => playSound('click')}
|
||||
>
|
||||
<Mic color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
<button className="icon-button" onClick={() => playSound('click')}>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
|
||||
<EmojiPicker
|
||||
visible={showEmoji}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { MessageList } from './message-list';
|
||||
import { ChatInput } from './chat-input';
|
||||
import { ChatHeader } from './chat-header';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { Message } from '../../types';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
export function ChatWindow() {
|
||||
const { line } = useChat();
|
||||
|
@ -14,7 +13,7 @@ export function ChatWindow() {
|
|||
React.useEffect(() => {
|
||||
if (!line) return;
|
||||
|
||||
const subscription = line.activity$.subscribe(activity => {
|
||||
const subscription = line.activity$.subscribe((activity: any) => {
|
||||
if (activity.type === 'message') {
|
||||
setMessages(prev => [...prev, activity as Message]);
|
||||
}
|
||||
|
@ -24,10 +23,10 @@ export function ChatWindow() {
|
|||
}, [line]);
|
||||
|
||||
return (
|
||||
<View style={chatStyles.window}>
|
||||
<div className="chat-window">
|
||||
<ChatHeader />
|
||||
<MessageList messages={messages} />
|
||||
<ChatInput />
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, View, Text } from 'react-native';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { useSound } from '../../providers/sound-provider';
|
||||
import { Message } from '../../types';
|
||||
import { chatStyles } from '../../styles/chat.styles';
|
||||
import '../../styles/chat.css';
|
||||
|
||||
interface MessageListProps {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export function MessageList({ messages }: MessageListProps) {
|
||||
const scrollViewRef = React.useRef<ScrollView>(null);
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const { user } = useChat();
|
||||
const { playSound } = useSound();
|
||||
const prevMessagesLength = React.useRef(messages.length);
|
||||
|
@ -21,33 +20,26 @@ export function MessageList({ messages }: MessageListProps) {
|
|||
if (lastMessage.from.id !== user.id) {
|
||||
playSound('receive');
|
||||
}
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
prevMessagesLength.current = messages.length;
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={chatStyles.messageList}
|
||||
contentContainerStyle={chatStyles.messageListContent}
|
||||
>
|
||||
<div className="message-list" ref={scrollRef}>
|
||||
{messages.map((message, index) => (
|
||||
<View
|
||||
<div
|
||||
key={`${message.id}-${index}`}
|
||||
style={[
|
||||
chatStyles.messageContainer,
|
||||
message.from.id === user.id
|
||||
? chatStyles.userMessage
|
||||
: chatStyles.botMessage
|
||||
]}
|
||||
className={`message-container ${
|
||||
message.from.id === user.id ? 'user-message' : 'bot-message'
|
||||
}`}
|
||||
>
|
||||
<Text style={chatStyles.messageText}>{message.text}</Text>
|
||||
<Text style={chatStyles.messageTime}>
|
||||
<p className="message-text">{message.text}</p>
|
||||
<span className="message-time">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</Text>
|
||||
</View>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Image } from 'react-native';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
interface ImageViewerProps {
|
||||
url: string;
|
||||
|
@ -8,12 +7,8 @@ interface ImageViewerProps {
|
|||
|
||||
export function ImageViewer({ url }: ImageViewerProps) {
|
||||
return (
|
||||
<View style={projectorStyles.imageContainer}>
|
||||
<Image
|
||||
source={{ uri: url }}
|
||||
style={projectorStyles.image}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<div className="image-container">
|
||||
<img src={url} className="projector-image" alt="Projected content" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import Markdown from 'react-native-markdown-display';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
interface MarkdownViewerProps {
|
||||
content: string;
|
||||
|
@ -9,10 +8,8 @@ interface MarkdownViewerProps {
|
|||
|
||||
export function MarkdownViewer({ content }: MarkdownViewerProps) {
|
||||
return (
|
||||
<ScrollView style={projectorStyles.markdownContainer}>
|
||||
<Markdown style={projectorStyles.markdown}>
|
||||
{content}
|
||||
</Markdown>
|
||||
</ScrollView>
|
||||
<div className="markdown-container">
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { VideoPlayer } from './video-player';
|
||||
import { ImageViewer } from './image-viewer';
|
||||
import { MarkdownViewer } from './markdown-viewer';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
export function ProjectorView() {
|
||||
const { line } = useChat();
|
||||
|
@ -14,9 +13,10 @@ export function ProjectorView() {
|
|||
if (!line) return;
|
||||
|
||||
const subscription = line.activity$
|
||||
.filter(activity => activity.type === 'event' && activity.name === 'project')
|
||||
.subscribe(activity => {
|
||||
setContent(activity.value);
|
||||
.subscribe((activity: any) => {
|
||||
if (activity.type === 'event' && activity.name === 'project') {
|
||||
setContent(activity.value);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
|
@ -38,8 +38,8 @@ export function ProjectorView() {
|
|||
};
|
||||
|
||||
return (
|
||||
<View style={projectorStyles.container}>
|
||||
<div className="projector-container">
|
||||
{renderContent()}
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
import React from 'react';
|
||||
import { View, Image } from 'react-native';
|
||||
import { projectorStyles } from '../../styles/projector.styles';
|
||||
import '../../styles/projector.css';
|
||||
|
||||
interface ImageViewerProps {
|
||||
interface VideoPlayerProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export function VideoViewer({ url }: ImageViewerProps) {
|
||||
export function VideoPlayer({ url }: VideoPlayerProps) {
|
||||
return (
|
||||
<View style={projectorStyles.imageContainer}>
|
||||
<iframe
|
||||
src={url}
|
||||
|
||||
|
||||
/>
|
||||
</View>
|
||||
<div className="video-container">
|
||||
<video controls src={url} className="projector-video" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,50 +1,44 @@
|
|||
import React from 'react';
|
||||
import { View, Text, Image, TextInput, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { Search } from 'lucide-react-native';
|
||||
import { useChat } from '../../providers/chat-provider';
|
||||
import { selectorStyles } from '../../styles/selector.styles';
|
||||
import '../../styles/selector.css';
|
||||
|
||||
export function PersonSelector() {
|
||||
const [search, setSearch] = React.useState('');
|
||||
const { instance } = useChat();
|
||||
|
||||
return (
|
||||
<View style={selectorStyles.container}>
|
||||
<View style={selectorStyles.header}>
|
||||
<Image
|
||||
source={{ uri: instance?.logo }}
|
||||
style={selectorStyles.logo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<div className="selector-container">
|
||||
<div className="selector-header">
|
||||
{instance?.logo && (
|
||||
<img src={instance.logo} className="selector-logo" alt="Logo" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<View style={selectorStyles.searchContainer}>
|
||||
<Search size={20} color="#00f3ff" />
|
||||
<TextInput
|
||||
<div className="search-container">
|
||||
<svg className="search-icon" viewBox="0 0 24 24">
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<input
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="search-input"
|
||||
placeholder="Search conversations..."
|
||||
placeholderTextColor="#666"
|
||||
style={selectorStyles.searchInput}
|
||||
/>
|
||||
</View>
|
||||
</div>
|
||||
|
||||
<ScrollView style={selectorStyles.list}>
|
||||
<div className="selector-list">
|
||||
{['FAQ', 'Support', 'Sales'].map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item}
|
||||
style={selectorStyles.item}
|
||||
>
|
||||
<View style={selectorStyles.avatar}>
|
||||
<Text style={selectorStyles.avatarText}>{item[0]}</Text>
|
||||
</View>
|
||||
<View style={selectorStyles.itemContent}>
|
||||
<Text style={selectorStyles.itemTitle}>{item}</Text>
|
||||
<Text style={selectorStyles.itemSubtitle}>Start a conversation</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<div key={item} className="selector-item">
|
||||
<div className="selector-avatar">
|
||||
<span>{item[0]}</span>
|
||||
</div>
|
||||
<div className="item-content">
|
||||
<h3 className="item-title">{item}</h3>
|
||||
<p className="item-subtitle">Start a conversation</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { soundAssets } from '../../../public/sounds/manifest';
|
||||
import { cacheAssets } from '../lib/asset-loader';
|
||||
|
||||
|
||||
export function SoundInitializer({ children }: { children: React.ReactNode }) {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
@ -11,31 +9,29 @@ export function SoundInitializer({ children }: { children: React.ReactNode }) {
|
|||
useEffect(() => {
|
||||
const initializeSounds = async () => {
|
||||
try {
|
||||
await cacheAssets(Object.values(soundAssets));
|
||||
await cacheAssets(Object.values(soundAssets));
|
||||
setIsReady(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to initialize sounds');
|
||||
}
|
||||
};
|
||||
|
||||
//initializeSounds();
|
||||
setIsReady(true);
|
||||
initializeSounds();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'red' }}>Error: {error}</Text>
|
||||
</View>
|
||||
<div className="error-container">
|
||||
<p className="error-text">Error: {error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text>Loading sounds...</Text>
|
||||
</View>
|
||||
<div className="loading-container">
|
||||
<p>Loading sounds...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Modal } from 'react-native';
|
||||
import { X } from 'lucide-react-native';
|
||||
import { emojiStyles } from '../../styles/ui.styles';
|
||||
import '../../styles/ui.css';
|
||||
|
||||
const EMOJI_CATEGORIES = {
|
||||
"😀 🎮": ["😀", "😎", "🤖", "👾", "🎮", "✨", "🚀", "💫"],
|
||||
|
@ -9,43 +7,45 @@ const EMOJI_CATEGORIES = {
|
|||
"🤖 🎯": ["🤖", "🎯", "🎲", "🎮", "🕹️", "👾", "💻", "⌨️"]
|
||||
};
|
||||
|
||||
export function EmojiPicker({ visible, onClose, onEmojiSelect }) {
|
||||
export function EmojiPicker({ visible, onClose, onEmojiSelect }: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
}) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
>
|
||||
<View style={emojiStyles.container}>
|
||||
<View style={emojiStyles.header}>
|
||||
<Text style={emojiStyles.title}>Select Emoji</Text>
|
||||
<TouchableOpacity onPress={onClose}>
|
||||
<X color="#00f3ff" size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={emojiStyles.content}>
|
||||
{Object.entries(EMOJI_CATEGORIES).map(([category, emojis]) => (
|
||||
<View key={category} style={emojiStyles.category}>
|
||||
<Text style={emojiStyles.categoryTitle}>{category}</Text>
|
||||
<View style={emojiStyles.emojiGrid}>
|
||||
{emojis.map(emoji => (
|
||||
<TouchableOpacity
|
||||
key={emoji}
|
||||
style={emojiStyles.emojiButton}
|
||||
onPress={() => {
|
||||
onEmojiSelect(emoji);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Text style={emojiStyles.emoji}>{emoji}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Modal>
|
||||
<div className="emoji-picker-modal">
|
||||
<div className="emoji-picker-header">
|
||||
<h3>Select Emoji</h3>
|
||||
<button onClick={onClose} className="close-button">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="emoji-picker-content">
|
||||
{Object.entries(EMOJI_CATEGORIES).map(([category, emojis]) => (
|
||||
<div key={category} className="emoji-category">
|
||||
<h4 className="category-title">{category}</h4>
|
||||
<div className="emoji-grid">
|
||||
{emojis.map(emoji => (
|
||||
<button
|
||||
key={emoji}
|
||||
className="emoji-button"
|
||||
onClick={() => {
|
||||
onEmojiSelect(emoji);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react';
|
||||
import { View, Text, Switch, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { Moon, Sun, Volume2, VolumeX, Zap, Settings } from 'lucide-react-native';
|
||||
import { useSound } from '../../providers/sound-provider';
|
||||
import { settingsStyles } from '../../styles/ui.styles';
|
||||
import '../../styles/ui.css';
|
||||
|
||||
export function SettingsPanel() {
|
||||
const [theme, setTheme] = React.useState('dark');
|
||||
|
@ -29,21 +27,27 @@ export function SettingsPanel() {
|
|||
};
|
||||
|
||||
return (
|
||||
// ... rest of the settings panel code ...
|
||||
<View style={settingsStyles.option}>
|
||||
{sound ? (
|
||||
<Volume2 color="#00f3ff" size={20} />
|
||||
) : (
|
||||
<VolumeX color="#666" size={20} />
|
||||
)}
|
||||
<Text style={settingsStyles.optionText}>Sound Effects</Text>
|
||||
<Switch
|
||||
value={sound}
|
||||
onValueChange={handleSoundToggle}
|
||||
trackColor={{ false: '#333', true: '#00f3ff44' }}
|
||||
thumbColor={sound ? '#00f3ff' : '#666'}
|
||||
/>
|
||||
</View>
|
||||
// ... rest of the settings panel code ...
|
||||
<div className="settings-panel">
|
||||
<div className="settings-option">
|
||||
{sound ? (
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M3 15a1 1 0 011-1h2.83a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4h3.55a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4H19a1 1 0 011 1v6a1 1 0 01-1 1h-2.83a1 1 0 00-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H9.72a1 1 0 01-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H4a1 1 0 01-1-1v-2zM12 6a1 1 0 011-1h6a1 1 0 011 1v3"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M3 15a1 1 0 011-1h2.83a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4h3.55a1 1 0 00.8-.4l1.12-1.5a1 1 0 01.8-.4H19a1 1 0 011 1v6a1 1 0 01-1 1h-2.83a1 1 0 00-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H9.72a1 1 0 01-.8-.4l-1.12-1.5a1 1 0 00-.8-.4H4a1 1 0 01-1-1v-2zM12 6a1 1 0 011-1h6a1 1 0 011 1v3M3 3l18 18"/>
|
||||
</svg>
|
||||
)}
|
||||
<span className="option-text">Sound Effects</span>
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sound}
|
||||
onChange={(e) => handleSoundToggle(e.target.checked)}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@ export function Chat() {
|
|||
return (
|
||||
<SoundInitializer>
|
||||
<SoundProvider>
|
||||
<ChatProvider>
|
||||
<ChatLayout />
|
||||
</ChatProvider>
|
||||
</SoundProvider>
|
||||
<ChatProvider>
|
||||
<ChatLayout />
|
||||
</ChatProvider>
|
||||
</SoundProvider>
|
||||
</SoundInitializer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import { DirectLine } from 'botframework-directlinejs';
|
||||
import { ChatInstance, User } from '../types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { User, ChatInstance } from '../types';
|
||||
|
||||
interface ChatContextType {
|
||||
line: DirectLine | null;
|
||||
line: any;
|
||||
user: User;
|
||||
instance: ChatInstance | null;
|
||||
sendActivity: (activity: any) => void;
|
||||
|
@ -12,92 +11,66 @@ interface ChatContextType {
|
|||
setVoice: (voice: any) => void;
|
||||
}
|
||||
|
||||
const generateUserId = () => {
|
||||
return 'usergb@gb';
|
||||
};
|
||||
|
||||
export const ChatContext = React.createContext<ChatContextType | undefined>(undefined);
|
||||
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
||||
|
||||
export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||
const [line, setLine] = React.useState<DirectLine | null>(null);
|
||||
const [instance, setInstance] = React.useState<ChatInstance | null>(null);
|
||||
const [line, setLine] = useState<any>(null);
|
||||
const [instance, setInstance] = useState<ChatInstance | null>(null);
|
||||
const [selectedVoice, setSelectedVoice] = useState(null);
|
||||
const [user] = React.useState<User>(() => ({
|
||||
const [user] = useState<User>({
|
||||
id: `user_${Math.random().toString(36).slice(2)}`,
|
||||
name: 'You'
|
||||
}));
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const initializeChat = async () => {
|
||||
try {
|
||||
const botId = window.location.pathname.split('/')[1] || 'default';
|
||||
const instanceData = await invoke('get_chat_instance', { botId });
|
||||
setInstance(instanceData as ChatInstance);
|
||||
|
||||
// Initialize DirectLine or other chat service
|
||||
const directLine = {
|
||||
activity$: { subscribe: () => {} },
|
||||
postActivity: () => ({ subscribe: () => {} })
|
||||
};
|
||||
setLine(directLine);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize chat:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeChat();
|
||||
}, []);
|
||||
|
||||
const initializeChat = async () => {
|
||||
const sendActivity = async (activity: any) => {
|
||||
try {
|
||||
var botId = window.location.href.split('/')[3];
|
||||
if (botId.indexOf('#') !== -1) {
|
||||
botId = botId.split('#')[0];
|
||||
}
|
||||
|
||||
if (!botId || botId === '') {
|
||||
botId = '[default]';
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
'http://localhost:4242/instances/' + botId,
|
||||
)
|
||||
const data = await response.json();
|
||||
const userId = generateUserId();
|
||||
|
||||
const directLine = data.webchatToken
|
||||
? new DirectLine({
|
||||
token: data.token,
|
||||
webSocket: true
|
||||
})
|
||||
: new DirectLine({
|
||||
domain: data.domain,
|
||||
secret: null,
|
||||
token: null,
|
||||
webSocket: false
|
||||
});
|
||||
directLine.setUserId(userId);
|
||||
setLine(directLine);
|
||||
setInstance(data.instance);
|
||||
console.info (`DirectLine for user:` + userId);
|
||||
await invoke('send_chat_activity', {
|
||||
activity: {
|
||||
...activity,
|
||||
from: user,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
line?.postActivity(activity).subscribe();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize chat:', error);
|
||||
console.error('Failed to send activity:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendActivity = (activity: any) => {
|
||||
line?.postActivity({
|
||||
...activity,
|
||||
from: user,
|
||||
timestamp: new Date().toISOString()
|
||||
}).subscribe();
|
||||
};
|
||||
|
||||
const setVoice = (voice: any) => {
|
||||
setSelectedVoice(voice);
|
||||
};
|
||||
|
||||
const contextValue: ChatContextType = {
|
||||
line,
|
||||
user,
|
||||
instance,
|
||||
sendActivity,
|
||||
selectedVoice,
|
||||
setVoice
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={contextValue}>
|
||||
<ChatContext.Provider value={{ line, user, instance, sendActivity, selectedVoice, setVoice }}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const context = React.useContext(ChatContext);
|
||||
const context = useContext(ChatContext);
|
||||
if (!context) {
|
||||
throw new Error('useChat must be used within ChatProvider');
|
||||
}
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import React from 'react';
|
||||
import React, { createContext, useContext, useCallback } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
interface SoundContextType {
|
||||
playSound: (sound: string) => void;
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const SoundContext = React.createContext<SoundContextType | undefined>(undefined);
|
||||
const SoundContext = createContext<SoundContextType | undefined>(undefined);
|
||||
|
||||
export function SoundProvider({ children }: { children: React.ReactNode }) {
|
||||
const playSound = React.useCallback((sound: string) => {
|
||||
// soundManager.play(sound as any);
|
||||
}, []);
|
||||
const [enabled, setEnabled] = React.useState(true);
|
||||
|
||||
const setEnabled = React.useCallback((enabled: boolean) => {
|
||||
// soundManager.setEnabled(enabled);
|
||||
}, []);
|
||||
const playSound = useCallback(async (sound: string) => {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
await invoke('play_sound', { sound });
|
||||
} catch (error) {
|
||||
console.error('Failed to play sound:', error);
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
return (
|
||||
<SoundContext.Provider value={{ playSound, setEnabled }}>
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const audioStyles = StyleSheet.create({
|
||||
volumeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
volumeButton: {
|
||||
padding: 8,
|
||||
},
|
||||
sliderContainer: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
height: 40,
|
||||
},
|
||||
visualizerContainer: {
|
||||
flexDirection: 'row',
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
visualizerBar: {
|
||||
width: 3,
|
||||
height: 20,
|
||||
backgroundColor: '#00f3ff',
|
||||
borderRadius: 2,
|
||||
marginHorizontal: 1,
|
||||
},
|
||||
modal: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||
margin: 20,
|
||||
marginTop: 100,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff',
|
||||
shadowColor: '#00f3ff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
voiceList: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
voiceOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
selectedVoice: {
|
||||
borderColor: '#00f3ff',
|
||||
backgroundColor: '#00f3ff11',
|
||||
},
|
||||
voiceInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
voiceName: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
voiceAccent: {
|
||||
color: '#666',
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#333',
|
||||
},
|
||||
title: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
closeButton: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
trigger: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 8,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
triggerText: {
|
||||
color: '#ffffff',
|
||||
marginLeft: 8,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export const voiceStyles = StyleSheet.create({
|
||||
// ... copy from audioStyles the modal-related styles ...
|
||||
// Add voice-specific styles here
|
||||
});
|
|
@ -1,91 +0,0 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const chatStyles = StyleSheet.create({
|
||||
window: {
|
||||
flex: 1,
|
||||
backgroundColor: '#111111',
|
||||
},
|
||||
messageList: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
messageListContent: {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
messageContainer: {
|
||||
maxWidth: '70%',
|
||||
marginVertical: 4,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
},
|
||||
userMessage: {
|
||||
alignSelf: 'flex-end',
|
||||
backgroundColor: '#00f3ff22',
|
||||
borderColor: '#00f3ff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
botMessage: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#bf00ff22',
|
||||
borderColor: '#bf00ff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
messageText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
},
|
||||
messageTime: {
|
||||
color: '#666666',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#1a1a1a',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
padding: 12,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 24,
|
||||
color: '#ffffff',
|
||||
maxHeight: 100,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#1a1a1a',
|
||||
},
|
||||
headerContent: {
|
||||
flex: 1,
|
||||
},
|
||||
headerTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
headerSubtitle: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 14,
|
||||
marginTop: 2,
|
||||
},
|
||||
headerButton: {
|
||||
padding: 8,
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: '#00f3ff22',
|
||||
borderRadius: 20,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff',
|
||||
}
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
// layout.styles.ts
|
||||
import { Colors } from '../../../constants/Colors';
|
||||
import { StyleSheet } from 'react-native';
|
||||
export const layoutStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: Colors.dark.background,
|
||||
},
|
||||
sidebar: {
|
||||
width: 300,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: Colors.dark.icon,
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
projector: {
|
||||
width: '40%',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: Colors.dark.icon,
|
||||
},
|
||||
chatArea: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
// projector.styles.ts
|
||||
import { Colors } from '../../../constants/Colors';
|
||||
import { StyleSheet } from 'react-native';
|
||||
export const projectorStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.dark.background,
|
||||
padding: 16,
|
||||
},
|
||||
videoContainer: {
|
||||
aspectRatio: 16/9,
|
||||
backgroundColor: Colors.dark.background,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imageContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.dark.background,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
markdownContainer: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
backgroundColor: Colors.dark.background,
|
||||
borderRadius: 8,
|
||||
},
|
||||
body: {
|
||||
color: Colors.dark.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
heading1: {
|
||||
color: Colors.dark.tint,
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
heading2: {
|
||||
color: Colors.dark.tint,
|
||||
fontSize: 20,
|
||||
marginBottom: 12,
|
||||
},
|
||||
link: {
|
||||
color: Colors.dark.tint,
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: Colors.dark.background,
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
}
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
// selector.styles.ts
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Colors } from '../../../constants/Colors';
|
||||
|
||||
export const selectorStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.dark.background,
|
||||
},
|
||||
header: {
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.dark.icon,
|
||||
},
|
||||
logo: {
|
||||
width: 150,
|
||||
height: 50,
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.dark.icon,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
color: Colors.dark.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.dark.icon,
|
||||
},
|
||||
avatar: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: Colors.dark.background,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.dark.tint,
|
||||
},
|
||||
avatarText: {
|
||||
color: Colors.dark.tint,
|
||||
fontSize: 20,
|
||||
},
|
||||
itemContent: {
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
itemTitle: {
|
||||
color: Colors.dark.text,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
itemSubtitle: {
|
||||
color: Colors.dark.icon,
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
|
@ -1,150 +0,0 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const emojiStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||
margin: 20,
|
||||
marginTop: 100,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff',
|
||||
shadowColor: '#00f3ff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#00f3ff33',
|
||||
},
|
||||
title: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
category: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
categoryTitle: {
|
||||
color: '#bf00ff',
|
||||
fontSize: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
emojiGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
emojiButton: {
|
||||
width: '12.5%',
|
||||
aspectRatio: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
},
|
||||
emoji: {
|
||||
fontSize: 24,
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#111111',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#00f3ff33',
|
||||
},
|
||||
title: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 12,
|
||||
},
|
||||
section: {
|
||||
padding: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: '#bf00ff',
|
||||
fontSize: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
option: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
optionText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
activeIndicator: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginLeft: 8,
|
||||
},
|
||||
effectPreview: {
|
||||
padding: 20,
|
||||
},
|
||||
previewTitle: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
previewContent: {
|
||||
padding: 20,
|
||||
backgroundColor: '#1a1a1a',
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#00f3ff33',
|
||||
},
|
||||
previewText: {
|
||||
color: '#00f3ff',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
// Add animation helpers
|
||||
export const pulseAnimation = {
|
||||
0: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
0.5: {
|
||||
opacity: 0.7,
|
||||
scale: 1.05,
|
||||
},
|
||||
1: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const neonGlow = {
|
||||
shadowColor: '#00f3ff',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.8,
|
||||
shadowRadius: 15,
|
||||
};
|
276
src/index.tsx
276
src/index.tsx
|
@ -1,28 +1,12 @@
|
|||
// app/index.tsx
|
||||
import {
|
||||
View,
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Text,
|
||||
Linking,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { globalStyles } from '../styles';
|
||||
import { styled } from 'nativewind';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import AuthenticationScreen from './authentication';
|
||||
import {Chat} from './chat';
|
||||
import { Chat } from './chat';
|
||||
import MailPage from './mail';
|
||||
import DashboardPage from './dashboard';
|
||||
import TaskPage from './tasks';
|
||||
import TemplatesPage from './templates';
|
||||
import DriveScreen from './drive';
|
||||
import SyncPage from './sync/page'; // Add this import
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
const StyledView = styled(View);
|
||||
import SyncPage from './sync/page';
|
||||
|
||||
const examples = [
|
||||
{ name: "Home", href: "authentication" },
|
||||
|
@ -33,204 +17,82 @@ const examples = [
|
|||
{ name: "Tasks", href: "tasks" },
|
||||
{ name: "Meet", href: "meet" },
|
||||
{ name: "Templates", href: "templates" },
|
||||
{ name: "Settings", href: "sync" }, // Changed from "settings" to "sync"
|
||||
{ name: "Settings", href: "sync" },
|
||||
{ name: "Help", href: "help" },
|
||||
];
|
||||
|
||||
interface ExamplesNavProps {
|
||||
style?: any;
|
||||
}
|
||||
|
||||
const ExamplesNav = ({ style }: ExamplesNavProps) => {
|
||||
const navigation = useNavigation();
|
||||
return (
|
||||
<StyledView style={[styles.examplesNavContainer, style]}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<StyledView style={styles.examplesNavInner}>
|
||||
{examples.map((example) => (
|
||||
<TouchableOpacity
|
||||
key={example.href}
|
||||
onPress={() => navigation.navigate(example.href)}
|
||||
style={styles.exampleButton}
|
||||
>
|
||||
<Text style={styles.exampleButtonText}>
|
||||
{example.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</StyledView>
|
||||
</ScrollView>
|
||||
</StyledView>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExampleCodeLinkProps {
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
const ExampleCodeLink = ({ pathname }: ExampleCodeLinkProps) => {
|
||||
const example = examples.find((example) => pathname.startsWith(example.href));
|
||||
|
||||
if (!example?.code) {
|
||||
return null;
|
||||
}
|
||||
const ExamplesNav = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL(example.code)}
|
||||
style={styles.exampleCodeLink}
|
||||
>
|
||||
<Text style={styles.exampleCodeLinkText}>View code</Text>
|
||||
<Text style={styles.exampleCodeLinkArrow}>→</Text>
|
||||
</TouchableOpacity>
|
||||
<div className="examples-nav-container">
|
||||
<div className="examples-nav-inner">
|
||||
{examples.map((example) => (
|
||||
<button
|
||||
key={example.href}
|
||||
onClick={() => navigate(example.href)}
|
||||
className={`example-button ${
|
||||
location.pathname.includes(example.href) ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
{example.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const navigatorOptions = {
|
||||
headerShown: false,
|
||||
cardOverlayEnabled: false,
|
||||
animation: 'fade',
|
||||
presentation: 'modal'
|
||||
};
|
||||
|
||||
const WrapWithExampleCodeLink = (WrappedComponent) => {
|
||||
return (props) => {
|
||||
const route = useRoute();
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<WrappedComponent {...props} />
|
||||
<ExampleCodeLink pathname={route.name} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
function RootLayout() {
|
||||
export function RootLayout() {
|
||||
return (
|
||||
<StyledView style={globalStyles.container}>
|
||||
<StatusBar style="auto" />
|
||||
<ClerkProvider publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY}>
|
||||
<ClerkLoaded>
|
||||
<SafeAreaView style={globalStyles.safeArea}>
|
||||
<ExamplesNav />
|
||||
<Stack.Navigator
|
||||
initialRouteName="authentication"
|
||||
screenOptions={navigatorOptions}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="authentication"
|
||||
component={WrapWithExampleCodeLink(AuthenticationScreen)}
|
||||
options={{ title: "Authentication" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="chat"
|
||||
component={WrapWithExampleCodeLink(Chat)}
|
||||
options={{ title: "Chat" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="dashboard"
|
||||
component={WrapWithExampleCodeLink(DashboardPage)}
|
||||
options={{ title: "Dashboard" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="mail"
|
||||
component={WrapWithExampleCodeLink(MailPage)}
|
||||
options={{ title: "Mail" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="drive"
|
||||
component={DriveScreen}
|
||||
options={{ title: "Drive" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="tasks"
|
||||
component={WrapWithExampleCodeLink(TaskPage)}
|
||||
options={{ title: "Tasks" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="meet"
|
||||
component={() => <Text>Meet Screen (Placeholder)</Text>}
|
||||
options={{ title: "Meet" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="templates"
|
||||
component={WrapWithExampleCodeLink(TemplatesPage)}
|
||||
options={{ title: "Templates" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="sync"
|
||||
component={SyncPage}
|
||||
options={{ title: "Cloud Sync" }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</SafeAreaView>
|
||||
</ClerkLoaded>
|
||||
</ClerkProvider>
|
||||
</StyledView>
|
||||
<div className="app-container">
|
||||
<ExamplesNav />
|
||||
<main className="app-main">
|
||||
<Routes>
|
||||
<Route path="authentication" element={
|
||||
<>
|
||||
<AuthenticationScreen />
|
||||
</>
|
||||
} />
|
||||
<Route path="chat" element={
|
||||
<>
|
||||
<Chat />
|
||||
|
||||
</>
|
||||
} />
|
||||
<Route path="dashboard" element={
|
||||
<>
|
||||
<DashboardPage />
|
||||
|
||||
</>
|
||||
} />
|
||||
<Route path="mail" element={
|
||||
<>
|
||||
<MailPage />
|
||||
|
||||
</>
|
||||
} />
|
||||
<Route path="drive" element={<DriveScreen />} />
|
||||
<Route path="tasks" element={
|
||||
<>
|
||||
<TaskPage />
|
||||
|
||||
</>
|
||||
} />
|
||||
<Route path="meet" element={<div>Meet Screen (Placeholder)</div>} />
|
||||
<Route path="templates" element={
|
||||
<>
|
||||
<TemplatesPage />
|
||||
|
||||
</>
|
||||
} />
|
||||
<Route path="sync" element={<SyncPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
examplesNavContainer: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
examplesNavInner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
exampleButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 6,
|
||||
marginRight: 4,
|
||||
},
|
||||
exampleButtonActive: {
|
||||
backgroundColor: '#f3f4f6',
|
||||
},
|
||||
exampleButtonText: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
},
|
||||
exampleButtonTextActive: {
|
||||
fontWeight: '500',
|
||||
color: '#6366f1',
|
||||
},
|
||||
exampleCodeLink: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
},
|
||||
exampleCodeLinkText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginRight: 4,
|
||||
color: '#6366f1',
|
||||
},
|
||||
exampleCodeLinkArrow: {
|
||||
fontSize: 14,
|
||||
color: '#6366f1',
|
||||
},
|
||||
logo: {
|
||||
marginLeft: 8,
|
||||
marginRight: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default RootLayout;
|
||||
export default RootLayout;
|
112
src/styles/chat.css
Normal file
112
src/styles/chat.css
Normal file
|
@ -0,0 +1,112 @@
|
|||
.chat-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: #00f3ff;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
max-width: 70%;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
align-self: flex-end;
|
||||
background-color: rgba(0, 243, 255, 0.1);
|
||||
border: 1px solid #00f3ff;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
align-self: flex-start;
|
||||
background-color: rgba(191, 0, 255, 0.1);
|
||||
border: 1px solid #bf00ff;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #666;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #333;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
min-height: 2.5rem;
|
||||
max-height: 6rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
color: white;
|
||||
border: 1px solid #333;
|
||||
border-radius: 1.25rem;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #00f3ff;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background-color: rgba(0, 243, 255, 0.1);
|
||||
border: 1px solid #00f3ff;
|
||||
border-radius: 50%;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: currentColor;
|
||||
}
|
24
src/styles/layout.css
Normal file
24
src/styles/layout.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.chat-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 18rem;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.projector {
|
||||
width: 40%;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
flex: 1;
|
||||
}
|
45
src/styles/projector.css
Normal file
45
src/styles/projector.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
.projector-container {
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.image-container, .video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projector-image, .projector-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.markdown-container {
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
color: white;
|
||||
background-color: #111;
|
||||
border-radius: 0.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.markdown-container h1,
|
||||
.markdown-container h2,
|
||||
.markdown-container h3 {
|
||||
color: #00f3ff;
|
||||
}
|
||||
|
||||
.markdown-container a {
|
||||
color: #00f3ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-container pre {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
87
src/styles/selector.css
Normal file
87
src/styles/selector.css
Normal file
|
@ -0,0 +1,87 @@
|
|||
.selector-container {
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.selector-logo {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.5rem;
|
||||
color: #00f3ff;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.selector-list {
|
||||
height: calc(100% - 7rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selector-item {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector-item:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.selector-avatar {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background-color: #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #00f3ff;
|
||||
color: #00f3ff;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-left: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-subtitle {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
137
src/styles/ui.css
Normal file
137
src/styles/ui.css
Normal file
|
@ -0,0 +1,137 @@
|
|||
.emoji-picker-modal {
|
||||
position: fixed;
|
||||
bottom: 5rem;
|
||||
right: 2rem;
|
||||
width: 20rem;
|
||||
max-height: 25rem;
|
||||
background-color: rgba(0, 0, 0, 0.95);
|
||||
border: 1px solid #00f3ff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0 1rem rgba(0, 243, 255, 0.5);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emoji-picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(0, 243, 255, 0.2);
|
||||
}
|
||||
|
||||
.emoji-picker-header h3 {
|
||||
color: #00f3ff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #00f3ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emoji-picker-content {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
max-height: calc(25rem - 3.5rem);
|
||||
}
|
||||
|
||||
.emoji-category {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
color: #bf00ff;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.emoji-button:hover {
|
||||
background-color: rgba(0, 243, 255, 0.1);
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background-color: #111;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.settings-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
color: white;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #333;
|
||||
transition: .4s;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
left: 0.2rem;
|
||||
bottom: 0.2rem;
|
||||
background-color: #666;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: rgba(0, 243, 255, 0.2);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(1.5rem);
|
||||
background-color: #00f3ff;
|
||||
}
|
Loading…
Add table
Reference in a new issue