refactor: migrate chat layout and styles to CSS, remove unused styles and components

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-03-30 19:04:24 -03:00
parent 0972670cf4
commit 4dc77b7717
33 changed files with 1984 additions and 1253 deletions

1131
a.sh Executable file

File diff suppressed because it is too large Load diff

View file

@ -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
View file

@ -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: {}

View file

@ -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
},
});

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -8,10 +8,10 @@ export function Chat() {
return (
<SoundInitializer>
<SoundProvider>
<ChatProvider>
<ChatLayout />
</ChatProvider>
</SoundProvider>
<ChatProvider>
<ChatLayout />
</ChatProvider>
</SoundProvider>
</SoundInitializer>
);
}

View file

@ -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');
}

View file

@ -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 }}>

View file

@ -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
});

View file

@ -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',
}
});

View file

@ -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,
},
});

View file

@ -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,
}
});

View file

@ -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,
},
});

View file

@ -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,
};

View file

@ -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
View 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
View 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
View 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
View 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
View 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;
}