1131 lines
No EOL
26 KiB
Bash
Executable file
1131 lines
No EOL
26 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
# Create the full directory structure
|
|
mkdir -p my-tauri-app/src/chat/{components,lib,providers,styles,types}
|
|
mkdir -p my-tauri-app/src/chat/components/{chat,projector,selector,ui}
|
|
mkdir -p my-tauri-app/src/styles
|
|
|
|
# Create all files with React+HTML equivalents
|
|
cat > my-tauri-app/src/chat/components/chat/chat-header.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { useChat } from '../../providers/chat-provider';
|
|
import '../../styles/chat.css';
|
|
|
|
export function ChatHeader() {
|
|
const { instance } = useChat();
|
|
|
|
return (
|
|
<div className="chat-header">
|
|
<div className="header-content">
|
|
<h2 className="header-title">
|
|
{instance?.name || 'Chat'}
|
|
</h2>
|
|
<span className="header-subtitle">Online</span>
|
|
</div>
|
|
|
|
<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>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/chat/chat-input.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { EmojiPicker } from '../ui/emoji-picker';
|
|
import { useChat } from '../../providers/chat-provider';
|
|
import { useSound } from '../../providers/sound-provider';
|
|
import '../../styles/chat.css';
|
|
|
|
export function ChatInput() {
|
|
const [message, setMessage] = React.useState('');
|
|
const [showEmoji, setShowEmoji] = React.useState(false);
|
|
const { sendActivity } = useChat();
|
|
const { playSound } = useSound();
|
|
|
|
const handleSend = () => {
|
|
if (!message.trim()) return;
|
|
playSound('send');
|
|
sendActivity({
|
|
type: 'message',
|
|
text: message.trim(),
|
|
});
|
|
setMessage('');
|
|
};
|
|
|
|
const handleEmojiSelect = (emoji: string) => {
|
|
playSound('click');
|
|
setMessage(prev => prev + emoji);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<textarea
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
className="chat-input"
|
|
placeholder="Type a message..."
|
|
/>
|
|
|
|
{message.trim().length > 0 ? (
|
|
<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>
|
|
) : (
|
|
<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>
|
|
)}
|
|
</div>
|
|
|
|
<EmojiPicker
|
|
visible={showEmoji}
|
|
onClose={() => {
|
|
playSound('click');
|
|
setShowEmoji(false);
|
|
}}
|
|
onEmojiSelect={handleEmojiSelect}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/chat/chat-window.tsx << 'EOL'
|
|
import React from 'react';
|
|
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 '../../styles/chat.css';
|
|
|
|
export function ChatWindow() {
|
|
const { line } = useChat();
|
|
const [messages, setMessages] = React.useState<Message[]>([]);
|
|
|
|
React.useEffect(() => {
|
|
if (!line) return;
|
|
|
|
const subscription = line.activity$.subscribe((activity: any) => {
|
|
if (activity.type === 'message') {
|
|
setMessages(prev => [...prev, activity as Message]);
|
|
}
|
|
});
|
|
|
|
return () => subscription.unsubscribe();
|
|
}, [line]);
|
|
|
|
return (
|
|
<div className="chat-window">
|
|
<ChatHeader />
|
|
<MessageList messages={messages} />
|
|
<ChatInput />
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/chat/message-list.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { useChat } from '../../providers/chat-provider';
|
|
import { useSound } from '../../providers/sound-provider';
|
|
import { Message } from '../../types';
|
|
import '../../styles/chat.css';
|
|
|
|
interface MessageListProps {
|
|
messages: Message[];
|
|
}
|
|
|
|
export function MessageList({ messages }: MessageListProps) {
|
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
|
const { user } = useChat();
|
|
const { playSound } = useSound();
|
|
const prevMessagesLength = React.useRef(messages.length);
|
|
|
|
React.useEffect(() => {
|
|
if (messages.length > prevMessagesLength.current) {
|
|
const lastMessage = messages[messages.length - 1];
|
|
if (lastMessage.from.id !== user.id) {
|
|
playSound('receive');
|
|
}
|
|
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
prevMessagesLength.current = messages.length;
|
|
}, [messages]);
|
|
|
|
return (
|
|
<div className="message-list" ref={scrollRef}>
|
|
{messages.map((message, index) => (
|
|
<div
|
|
key={`${message.id}-${index}`}
|
|
className={`message-container ${
|
|
message.from.id === user.id ? 'user-message' : 'bot-message'
|
|
}`}
|
|
>
|
|
<p className="message-text">{message.text}</p>
|
|
<span className="message-time">
|
|
{new Date(message.timestamp).toLocaleTimeString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/chat-layout.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { PersonSelector } from './selector/person-selector';
|
|
import { ProjectorView } from './projector/projector-view';
|
|
import { ChatWindow } from './chat/chat-window';
|
|
import '../../styles/layout.css';
|
|
|
|
export function ChatLayout() {
|
|
return (
|
|
<div className="chat-layout">
|
|
<div className="sidebar">
|
|
<PersonSelector />
|
|
</div>
|
|
<div className="main-content">
|
|
<div className="projector">
|
|
<ProjectorView />
|
|
</div>
|
|
<div className="chat-area">
|
|
<ChatWindow />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/projector/image-viewer.tsx << 'EOL'
|
|
import React from 'react';
|
|
import '../../styles/projector.css';
|
|
|
|
interface ImageViewerProps {
|
|
url: string;
|
|
}
|
|
|
|
export function ImageViewer({ url }: ImageViewerProps) {
|
|
return (
|
|
<div className="image-container">
|
|
<img src={url} className="projector-image" alt="Projected content" />
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/projector/markdown-viewer.tsx << 'EOL'
|
|
import React from 'react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import '../../styles/projector.css';
|
|
|
|
interface MarkdownViewerProps {
|
|
content: string;
|
|
}
|
|
|
|
export function MarkdownViewer({ content }: MarkdownViewerProps) {
|
|
return (
|
|
<div className="markdown-container">
|
|
<ReactMarkdown>{content}</ReactMarkdown>
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/projector/projector-view.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { VideoPlayer } from './video-player';
|
|
import { ImageViewer } from './image-viewer';
|
|
import { MarkdownViewer } from './markdown-viewer';
|
|
import { useChat } from '../../providers/chat-provider';
|
|
import '../../styles/projector.css';
|
|
|
|
export function ProjectorView() {
|
|
const { line } = useChat();
|
|
const [content, setContent] = React.useState<any>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!line) return;
|
|
|
|
const subscription = line.activity$
|
|
.subscribe((activity: any) => {
|
|
if (activity.type === 'event' && activity.name === 'project') {
|
|
setContent(activity.value);
|
|
}
|
|
});
|
|
|
|
return () => subscription.unsubscribe();
|
|
}, [line]);
|
|
|
|
const renderContent = () => {
|
|
if (!content) return null;
|
|
|
|
switch (content.type) {
|
|
case 'video':
|
|
return <VideoPlayer url={content.url} />;
|
|
case 'image':
|
|
return <ImageViewer url={content.url} />;
|
|
case 'markdown':
|
|
return <MarkdownViewer content={content.text} />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="projector-container">
|
|
{renderContent()}
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/projector/video-player.tsx << 'EOL'
|
|
import React from 'react';
|
|
import '../../styles/projector.css';
|
|
|
|
interface VideoPlayerProps {
|
|
url: string;
|
|
}
|
|
|
|
export function VideoPlayer({ url }: VideoPlayerProps) {
|
|
return (
|
|
<div className="video-container">
|
|
<video controls src={url} className="projector-video" />
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/selector/person-selector.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { useChat } from '../../providers/chat-provider';
|
|
import '../../styles/selector.css';
|
|
|
|
export function PersonSelector() {
|
|
const [search, setSearch] = React.useState('');
|
|
const { instance } = useChat();
|
|
|
|
return (
|
|
<div className="selector-container">
|
|
<div className="selector-header">
|
|
{instance?.logo && (
|
|
<img src={instance.logo} className="selector-logo" alt="Logo" />
|
|
)}
|
|
</div>
|
|
|
|
<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}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="search-input"
|
|
placeholder="Search conversations..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="selector-list">
|
|
{['FAQ', 'Support', 'Sales'].map((item) => (
|
|
<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>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/sound-initializer.tsx << 'EOL'
|
|
import React, { useEffect, useState } from 'react';
|
|
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);
|
|
|
|
useEffect(() => {
|
|
const initializeSounds = async () => {
|
|
try {
|
|
await cacheAssets(Object.values(soundAssets));
|
|
setIsReady(true);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to initialize sounds');
|
|
}
|
|
};
|
|
|
|
initializeSounds();
|
|
}, []);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="error-container">
|
|
<p className="error-text">Error: {error}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isReady) {
|
|
return (
|
|
<div className="loading-container">
|
|
<p>Loading sounds...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/ui/emoji-picker.tsx << 'EOL'
|
|
import React from 'react';
|
|
import '../../styles/ui.css';
|
|
|
|
const EMOJI_CATEGORIES = {
|
|
"😀 🎮": ["😀", "😎", "🤖", "👾", "🎮", "✨", "🚀", "💫"],
|
|
"🌟 💫": ["⭐", "🌟", "💫", "✨", "⚡", "💥", "🔥", "🌈"],
|
|
"🤖 🎯": ["🤖", "🎯", "🎲", "🎮", "🕹️", "👾", "💻", "⌨️"]
|
|
};
|
|
|
|
export function EmojiPicker({ visible, onClose, onEmojiSelect }: {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
onEmojiSelect: (emoji: string) => void;
|
|
}) {
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/components/ui/settings-panel.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { useSound } from '../../providers/sound-provider';
|
|
import '../../styles/ui.css';
|
|
|
|
export function SettingsPanel() {
|
|
const [theme, setTheme] = React.useState('dark');
|
|
const [sound, setSound] = React.useState(true);
|
|
const [powerMode, setPowerMode] = React.useState(false);
|
|
const { setEnabled, playSound } = useSound();
|
|
|
|
const handleSoundToggle = (value: boolean) => {
|
|
setSound(value);
|
|
setEnabled(value);
|
|
if (value) {
|
|
playSound('success');
|
|
}
|
|
};
|
|
|
|
const handleThemeChange = () => {
|
|
playSound('click');
|
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
};
|
|
|
|
const handlePowerMode = (value: boolean) => {
|
|
playSound(value ? 'success' : 'click');
|
|
setPowerMode(value);
|
|
};
|
|
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/index.tsx << 'EOL'
|
|
import React from 'react';
|
|
import { ChatProvider } from './providers/chat-provider';
|
|
import { ChatLayout } from './components/chat-layout';
|
|
import { SoundInitializer } from './components/sound-initializer';
|
|
import { SoundProvider } from './providers/sound-provider';
|
|
|
|
export function Chat() {
|
|
return (
|
|
<SoundInitializer>
|
|
<SoundProvider>
|
|
<ChatProvider>
|
|
<ChatLayout />
|
|
</ChatProvider>
|
|
</SoundProvider>
|
|
</SoundInitializer>
|
|
);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/lib/utils.ts << 'EOL'
|
|
export function formatTimestamp(date: Date): string {
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(date);
|
|
}
|
|
|
|
export function cn(...classes: string[]): string {
|
|
return classes.filter(Boolean).join(' ');
|
|
}
|
|
|
|
export function generateId(): string {
|
|
return Math.random().toString(36).slice(2);
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/providers/chat-provider.tsx << 'EOL'
|
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
import { invoke } from '@tauri-apps/api/tauri';
|
|
import { User, ChatInstance } from '../types';
|
|
|
|
interface ChatContextType {
|
|
line: any;
|
|
user: User;
|
|
instance: ChatInstance | null;
|
|
sendActivity: (activity: any) => void;
|
|
selectedVoice: any;
|
|
setVoice: (voice: any) => void;
|
|
}
|
|
|
|
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
|
|
|
export function ChatProvider({ children }: { children: React.ReactNode }) {
|
|
const [line, setLine] = useState<any>(null);
|
|
const [instance, setInstance] = useState<ChatInstance | null>(null);
|
|
const [selectedVoice, setSelectedVoice] = useState(null);
|
|
const [user] = useState<User>({
|
|
id: `user_${Math.random().toString(36).slice(2)}`,
|
|
name: 'You'
|
|
});
|
|
|
|
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 sendActivity = async (activity: any) => {
|
|
try {
|
|
await invoke('send_chat_activity', {
|
|
activity: {
|
|
...activity,
|
|
from: user,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
});
|
|
line?.postActivity(activity).subscribe();
|
|
} catch (error) {
|
|
console.error('Failed to send activity:', error);
|
|
}
|
|
};
|
|
|
|
const setVoice = (voice: any) => {
|
|
setSelectedVoice(voice);
|
|
};
|
|
|
|
return (
|
|
<ChatContext.Provider value={{ line, user, instance, sendActivity, selectedVoice, setVoice }}>
|
|
{children}
|
|
</ChatContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useChat() {
|
|
const context = useContext(ChatContext);
|
|
if (!context) {
|
|
throw new Error('useChat must be used within ChatProvider');
|
|
}
|
|
return context;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/providers/sound-provider.tsx << 'EOL'
|
|
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 = createContext<SoundContextType | undefined>(undefined);
|
|
|
|
export function SoundProvider({ children }: { children: React.ReactNode }) {
|
|
const [enabled, setEnabled] = React.useState(true);
|
|
|
|
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 }}>
|
|
{children}
|
|
</SoundContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useSound() {
|
|
const context = React.useContext(SoundContext);
|
|
if (!context) {
|
|
throw new Error('useSound must be used within SoundProvider');
|
|
}
|
|
return context;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/chat/types/index.ts << 'EOL'
|
|
export interface Message {
|
|
id: string;
|
|
type: 'message' | 'event';
|
|
text?: string;
|
|
timestamp: string;
|
|
from: User;
|
|
attachments?: Attachment[];
|
|
}
|
|
|
|
export interface User {
|
|
id: string;
|
|
name: string;
|
|
avatar?: string;
|
|
}
|
|
|
|
export interface Attachment {
|
|
type: 'image' | 'video' | 'document';
|
|
url: string;
|
|
thumbnailUrl?: string;
|
|
}
|
|
|
|
export interface ChatInstance {
|
|
id: string;
|
|
name: string;
|
|
logo?: string;
|
|
botId: string;
|
|
webchatToken: string;
|
|
}
|
|
EOL
|
|
|
|
# Create CSS files
|
|
cat > my-tauri-app/src/styles/chat.css << 'EOL'
|
|
.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;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/styles/layout.css << 'EOL'
|
|
.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;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/styles/projector.css << 'EOL'
|
|
.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;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/styles/selector.css << 'EOL'
|
|
.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;
|
|
}
|
|
EOL
|
|
|
|
cat > my-tauri-app/src/styles/ui.css << 'EOL'
|
|
.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;
|
|
}
|
|
EOL
|
|
|
|
echo "All files have been created with React + Tauri compatible versions!" |