feat: setup React/Vite frontend with Tyndale AI chat interface
Initialize complete frontend project structure with the following components: - React 18 + Vite development environment with TypeScript - Tailwind CSS for styling with custom animations - Radix UI components for accessible UI primitives - React Router for navigation between landing and chat pages - TanStack Query for efficient API data management Key features implemented: - Landing page with hero, features, and footer sections - Real-time chat interface with message history - Study/Trading mode toggle for different interaction types - Custom hooks for chat state management - API integration layer with backend service - Responsive design with scroll areas and card layouts Docker deployment configuration: - Multi-stage Dockerfile with Node.js build and Nginx production - Custom nginx configuration for SPA routing on port 3000 - Optimized production build process Development tools: - ESLint for code quality - TypeScript for type safety - PostCSS with Autoprefixer - Environment variable configuration with .env.example
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Toaster } from 'sonner'
|
||||
import LandingPage from './pages/LandingPage'
|
||||
import ChatPage from './pages/ChatPage'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/chat" element={<ChatPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState, KeyboardEvent } from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Send } from 'lucide-react'
|
||||
import type { ChatMode } from '@/types/chat'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void
|
||||
disabled: boolean
|
||||
mode: ChatMode
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const ChatInput = ({ onSend, disabled, mode, placeholder = 'Ask a question...' }: ChatInputProps) => {
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const handleSend = () => {
|
||||
if (message.trim() && !disabled) {
|
||||
onSend(message)
|
||||
setMessage('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
const modeFocusColors = {
|
||||
general: 'focus-visible:ring-blue-500',
|
||||
strategy: 'focus-visible:ring-purple-500',
|
||||
}
|
||||
|
||||
const modeButtonColors = {
|
||||
general: 'bg-blue-500 hover:bg-blue-600',
|
||||
strategy: 'bg-purple-500 hover:bg-purple-600',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex-1 bg-slate-700 border-slate-600 text-white placeholder:text-slate-400',
|
||||
modeFocusColors[mode]
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={disabled || !message.trim()}
|
||||
className={cn('rounded-xl', modeButtonColors[mode])}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Message, ChatMode } from '@/types/chat'
|
||||
import { User, Bot } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message
|
||||
mode: ChatMode
|
||||
}
|
||||
|
||||
const ChatMessage = ({ message, mode }: ChatMessageProps) => {
|
||||
const isUser = message.role === 'user'
|
||||
|
||||
const modeColors = {
|
||||
general: {
|
||||
userBg: 'bg-blue-600 border-blue-500/30',
|
||||
userText: 'text-white',
|
||||
},
|
||||
strategy: {
|
||||
userBg: 'bg-purple-600 border-purple-500/30',
|
||||
userText: 'text-white',
|
||||
},
|
||||
}
|
||||
|
||||
const colors = modeColors[mode]
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-3 mb-4', isUser ? 'justify-end' : 'justify-start')}>
|
||||
{!isUser && (
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-slate-700 flex items-center justify-center">
|
||||
<Bot className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[80%] rounded-2xl px-4 py-3 border',
|
||||
isUser
|
||||
? `${colors.userBg} ${colors.userText}`
|
||||
: 'bg-slate-700 text-white border-slate-600'
|
||||
)}
|
||||
>
|
||||
{isUser ? (
|
||||
<p className="text-sm">{message.content}</p>
|
||||
) : (
|
||||
<div className="prose prose-invert prose-sm max-w-none">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isUser && (
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-slate-600 flex items-center justify-center">
|
||||
<User className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatMessage
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { ChatMode } from '@/types/chat'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ModeToggleProps {
|
||||
mode: ChatMode
|
||||
onToggle: () => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
const ModeToggle = ({ mode, onToggle, disabled }: ModeToggleProps) => {
|
||||
const [isRippling, setIsRippling] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
if (!disabled) {
|
||||
setIsRippling(true)
|
||||
setTimeout(() => setIsRippling(false), 350)
|
||||
onToggle()
|
||||
}
|
||||
}
|
||||
|
||||
const modeConfig = {
|
||||
general: {
|
||||
label: 'General Assistant',
|
||||
bg: 'bg-blue-500/20 border-blue-500/40',
|
||||
text: 'text-blue-400',
|
||||
hover: 'hover:bg-blue-500/30',
|
||||
},
|
||||
strategy: {
|
||||
label: 'Strategy Analysis',
|
||||
bg: 'bg-purple-500/20 border-purple-500/40',
|
||||
text: 'text-purple-400',
|
||||
hover: 'hover:bg-purple-500/30',
|
||||
},
|
||||
}
|
||||
|
||||
const config = modeConfig[mode]
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'rounded-xl border-2 font-medium transition-all',
|
||||
config.bg,
|
||||
config.text,
|
||||
config.hover,
|
||||
isRippling && 'text-ripple-animate'
|
||||
)}
|
||||
>
|
||||
{config.label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModeToggle
|
||||
@@ -0,0 +1,167 @@
|
||||
interface Candle {
|
||||
id: number
|
||||
x: number
|
||||
y: number
|
||||
open: number
|
||||
close: number
|
||||
high: number
|
||||
low: number
|
||||
delay: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
// Generate static candlesticks once
|
||||
const generateCandles = (): Candle[] => {
|
||||
const newCandles: Candle[] = []
|
||||
const numCandles = 15
|
||||
|
||||
for (let i = 0; i < numCandles; i++) {
|
||||
const open = Math.random() * 60 + 20
|
||||
const movement = (Math.random() - 0.5) * 40
|
||||
const close = open + movement
|
||||
const high = Math.max(open, close) + Math.random() * 20
|
||||
const low = Math.min(open, close) - Math.random() * 20
|
||||
|
||||
newCandles.push({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
open,
|
||||
close,
|
||||
high,
|
||||
low,
|
||||
delay: Math.random() * 5,
|
||||
duration: 8 + Math.random() * 4,
|
||||
})
|
||||
}
|
||||
return newCandles
|
||||
}
|
||||
|
||||
const CANDLES = generateCandles()
|
||||
|
||||
const TradingBackground = () => {
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bullishGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor="rgba(16, 185, 129, 0.5)" />
|
||||
<stop offset="100%" stopColor="rgba(16, 185, 129, 0.2)" />
|
||||
</linearGradient>
|
||||
<linearGradient id="bearishGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor="rgba(239, 68, 68, 0.5)" />
|
||||
<stop offset="100%" stopColor="rgba(239, 68, 68, 0.2)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{CANDLES.map((candle) => {
|
||||
const isBullish = candle.close > candle.open
|
||||
const color = isBullish ? 'url(#bullishGradient)' : 'url(#bearishGradient)'
|
||||
const strokeColor = isBullish ? 'rgba(16, 185, 129, 0.6)' : 'rgba(239, 68, 68, 0.6)'
|
||||
|
||||
const bodyTop = Math.min(candle.open, candle.close)
|
||||
const bodyHeight = Math.abs(candle.close - candle.open)
|
||||
const bodyWidth = 1.5
|
||||
|
||||
return (
|
||||
<g
|
||||
key={candle.id}
|
||||
className="animate-pulse"
|
||||
style={{
|
||||
animationDelay: `${candle.delay}s`,
|
||||
animationDuration: `${candle.duration}s`,
|
||||
}}
|
||||
>
|
||||
{/* High-Low line (wick) */}
|
||||
<line
|
||||
x1={candle.x}
|
||||
y1={candle.y - (candle.high - candle.close) / 3}
|
||||
x2={candle.x}
|
||||
y2={candle.y + (candle.close - candle.low) / 3}
|
||||
stroke={strokeColor}
|
||||
strokeWidth="0.3"
|
||||
/>
|
||||
|
||||
{/* Candle body */}
|
||||
<rect
|
||||
x={candle.x - bodyWidth / 2}
|
||||
y={candle.y - (bodyTop - candle.close) / 3}
|
||||
width={bodyWidth}
|
||||
height={bodyHeight / 3}
|
||||
fill={color}
|
||||
stroke={strokeColor}
|
||||
strokeWidth="0.15"
|
||||
rx="0.15"
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Animated trend lines - now spanning full width */}
|
||||
<g className="animate-pulse" style={{ animationDuration: '6s' }}>
|
||||
<path
|
||||
d="M 0,30 Q 25,15 50,20 T 100,10"
|
||||
stroke="rgba(96, 165, 250, 0.4)"
|
||||
strokeWidth="0.5"
|
||||
fill="none"
|
||||
strokeDasharray="3,3"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="0"
|
||||
to="6"
|
||||
dur="3s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
|
||||
<g className="animate-pulse" style={{ animationDuration: '8s', animationDelay: '1s' }}>
|
||||
<path
|
||||
d="M 0,70 Q 25,85 50,80 T 100,95"
|
||||
stroke="rgba(168, 85, 247, 0.4)"
|
||||
strokeWidth="0.5"
|
||||
fill="none"
|
||||
strokeDasharray="3,3"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="0"
|
||||
to="6"
|
||||
dur="3s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
|
||||
<g className="animate-pulse" style={{ animationDuration: '7s', animationDelay: '0.5s' }}>
|
||||
<path
|
||||
d="M 0,50 Q 30,40 60,45 Q 80,50 100,55"
|
||||
stroke="rgba(59, 130, 246, 0.35)"
|
||||
strokeWidth="0.4"
|
||||
fill="none"
|
||||
strokeDasharray="2.5,2.5"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="0"
|
||||
to="5"
|
||||
dur="4s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{/* Floating gradient orbs */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDuration: '8s' }}></div>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDuration: '10s', animationDelay: '2s' }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TradingBackground
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import type { ChatMode } from '@/types/chat'
|
||||
import { useChat } from '@/hooks/useChat'
|
||||
import { apiClient } from '@/lib/api'
|
||||
import ChatMessage from './ChatMessage'
|
||||
import ChatInput from './ChatInput'
|
||||
import ModeToggle from './ModeToggle'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
|
||||
const TyndaleBot = () => {
|
||||
const [mode, setMode] = useState<ChatMode>('general')
|
||||
const { messages, isLoading, sendMessage, clearMessages } = useChat(mode)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
const handleModeToggle = () => {
|
||||
const newMode: ChatMode = mode === 'general' ? 'strategy' : 'general'
|
||||
|
||||
// Confirm with user before clearing conversation
|
||||
if (messages.length > 0) {
|
||||
const confirmed = window.confirm(
|
||||
'Switching modes will clear your current conversation. Continue?'
|
||||
)
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
setMode(newMode)
|
||||
clearMessages()
|
||||
apiClient.clearSessionId() // Start fresh session
|
||||
}
|
||||
|
||||
const scrollbarClass = mode === 'general' ? 'tyndale-scroll-general' : 'tyndale-scroll-strategy'
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<div className="tech-border-wrapper w-full max-w-4xl h-[85vh]">
|
||||
<div className="bg-slate-800 rounded-lg h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-700">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/" className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<h2 className="text-xl font-display font-bold text-white">
|
||||
Woolnoth, LLC
|
||||
</h2>
|
||||
</div>
|
||||
<ModeToggle mode={mode} onToggle={handleModeToggle} disabled={isLoading} />
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div ref={scrollRef} className={cn('space-y-4', scrollbarClass)}>
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-center">
|
||||
<div className="max-w-md">
|
||||
<h3 className="text-2xl font-display font-bold text-white mb-4">
|
||||
{mode === 'general' ? 'General Trading Assistant' : 'Strategy Analysis'}
|
||||
</h3>
|
||||
<p className="text-slate-400">
|
||||
{mode === 'general'
|
||||
? 'Ask me anything about trading concepts, market conditions, or terminology.'
|
||||
: 'Let\'s analyze your algorithmic strategies, backtest results, and risk metrics.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<ChatMessage key={message.id} message={message} mode={mode} />
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoading && (
|
||||
<div className="flex gap-3 mb-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-slate-700 flex items-center justify-center">
|
||||
<div className="flex gap-1">
|
||||
<div
|
||||
className={cn(
|
||||
'w-1.5 h-1.5 rounded-full animate-bounce',
|
||||
mode === 'general' ? 'bg-blue-400' : 'bg-purple-400'
|
||||
)}
|
||||
style={{ animationDelay: '0ms' }}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'w-1.5 h-1.5 rounded-full animate-bounce',
|
||||
mode === 'general' ? 'bg-blue-500' : 'bg-purple-500'
|
||||
)}
|
||||
style={{ animationDelay: '150ms' }}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'w-1.5 h-1.5 rounded-full animate-bounce',
|
||||
mode === 'general' ? 'bg-blue-600' : 'bg-purple-600'
|
||||
)}
|
||||
style={{ animationDelay: '300ms' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-slate-700">
|
||||
<ChatInput
|
||||
onSend={sendMessage}
|
||||
disabled={isLoading}
|
||||
mode={mode}
|
||||
placeholder={
|
||||
mode === 'general'
|
||||
? 'Ask about trading concepts...'
|
||||
: 'Ask about strategy analysis...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TyndaleBot
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
import { MessageSquare, TrendingUp, Zap } from 'lucide-react'
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: 'General Trading Assistant',
|
||||
description: 'Ask questions about trading concepts, market conditions, and terminology. Get instant, accurate answers.'
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
title: 'Strategy Analysis',
|
||||
description: 'Analyze algorithmic strategies, backtest insights, and risk assessment with AI-powered recommendations.'
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Real-Time Insights',
|
||||
description: 'Streaming responses with contextual market data. Experience conversational AI that keeps up with the markets.'
|
||||
}
|
||||
]
|
||||
|
||||
const LandingFeatures = () => {
|
||||
return (
|
||||
<section id="features" className="py-24 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Powerful Features
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Everything you need to make informed trading decisions with AI assistance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="group hover:shadow-xl transition-all duration-300 hover:-translate-y-2 animate-fade-up border-2"
|
||||
style={{animationDelay: `${index * 0.1}s`}}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
||||
<feature.icon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-xl font-display">{feature.title}</CardTitle>
|
||||
<CardDescription className="text-gray-600">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingFeatures
|
||||
@@ -0,0 +1,31 @@
|
||||
const LandingFooter = () => {
|
||||
return (
|
||||
<footer className="bg-gray-900 text-gray-400 py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between">
|
||||
<div className="mb-4 md:mb-0">
|
||||
<span className="text-2xl font-display font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
|
||||
Woolnoth, LLC
|
||||
</span>
|
||||
<p className="text-sm mt-2">Algorithmic Trading Intelligence</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Privacy Policy
|
||||
</a>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Terms of Service
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-800 text-center text-sm">
|
||||
<p>© 2026 Woolnoth, LLC. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingFooter
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import TradingBackground from '@/components/chat/TradingBackground'
|
||||
|
||||
const LandingHero = () => {
|
||||
return (
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden grain-overlay bg-gradient-to-br from-trading-dark via-trading-blue to-trading-dark">
|
||||
<TradingBackground />
|
||||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center pt-20">
|
||||
<h1 className="text-5xl md:text-7xl font-display font-bold text-white mb-6 animate-fade-up">
|
||||
Algorithmic Trading <br />
|
||||
<span className="bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
Intelligence
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-gray-300 mb-10 max-w-3xl mx-auto animate-fade-up" style={{animationDelay: '0.1s'}}>
|
||||
Your AI assistant for strategy analysis, market insights, and trading decisions
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 animate-fade-up" style={{animationDelay: '0.2s'}}>
|
||||
<Link to="/chat">
|
||||
<Button className="bg-violet-500 hover:bg-violet-600 text-white rounded-xl px-8 py-6 text-lg shadow-glow-purple">
|
||||
Start Trading Chat
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingHero
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const LandingNavigation = () => {
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link to="/" className="flex items-center space-x-2">
|
||||
<span className="text-2xl font-display font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
Woolnoth, LLC
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link to="/chat">
|
||||
<Button className="bg-violet-500 hover:bg-violet-600 text-white rounded-xl">
|
||||
Start
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingNavigation
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react'
|
||||
import type { Message, ChatMode } from '@/types/chat'
|
||||
import { apiClient } from '@/lib/api'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export const useChat = (mode: ChatMode) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const sendMessage = async (question: string) => {
|
||||
if (!question.trim()) {
|
||||
toast.error('Please enter a message')
|
||||
return
|
||||
}
|
||||
|
||||
// Add user message
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: question,
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, userMessage])
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Create placeholder assistant message
|
||||
const assistantMessageId = (Date.now() + 1).toString()
|
||||
let assistantContent = ''
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: assistantMessageId,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
},
|
||||
])
|
||||
|
||||
// Process SSE stream
|
||||
const streamGenerator = apiClient.chatStream(question, mode)
|
||||
|
||||
for await (const chunk of streamGenerator) {
|
||||
if (chunk.type === 'chunk') {
|
||||
// Append chunk content
|
||||
assistantContent += chunk.data
|
||||
|
||||
// Update assistant message
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === assistantMessageId
|
||||
? { ...msg, content: assistantContent }
|
||||
: msg
|
||||
)
|
||||
)
|
||||
} else if (chunk.type === 'error') {
|
||||
toast.error(chunk.data)
|
||||
console.error('Stream error:', chunk.data)
|
||||
} else if (chunk.type === 'done') {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error)
|
||||
toast.error('Failed to send message. Please try again.')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const clearMessages = () => {
|
||||
setMessages([])
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
isLoading,
|
||||
sendMessage,
|
||||
clearMessages,
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground font-montserrat;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-display;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Grain texture overlay */
|
||||
.grain-overlay {
|
||||
position: relative;
|
||||
}
|
||||
.grain-overlay::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMjAwIDIwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48ZmlsdGVyIGlkPSdub2lzZSc+PGZlVHVyYnVsZW5jZSB0eXBlPSdmcmFjdGFsTm9pc2UnIGJhc2VGcmVxdWVuY3k9JzAuNjUnIG51bU9jdGF2ZXM9JzMnIHN0aXRjaFRpbGVzPSdzdGl0Y2gnLz48L2ZpbHRlcj48cmVjdCB3aWR0aD0nMTAwJScgaGVpZ2h0PScxMDAlJyBmaWx0ZXI9J3VybCgjbm9pc2UpJy8+PC9zdmc+");
|
||||
opacity: 0.03;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Glass effect */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Text ripple animation for mode toggle */
|
||||
.text-ripple-animate {
|
||||
animation: text-ripple 350ms ease-out;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* Custom scrollbars for chat modes */
|
||||
.tyndale-scroll {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.tyndale-scroll::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.tyndale-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* General Mode - Blue scrollbar */
|
||||
.tyndale-scroll-general {
|
||||
scrollbar-color: rgb(59 130 246 / 0.7) transparent;
|
||||
}
|
||||
|
||||
.tyndale-scroll-general::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(to bottom, rgb(96 165 250), rgb(59 130 246));
|
||||
border-radius: 9999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Strategy Mode - Purple scrollbar */
|
||||
.tyndale-scroll-strategy {
|
||||
scrollbar-color: rgb(168 85 247 / 0.7) transparent;
|
||||
}
|
||||
|
||||
.tyndale-scroll-strategy::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(to bottom, rgb(192 132 252), rgb(168 85 247));
|
||||
border-radius: 9999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* Animated gradient border for chat window */
|
||||
@keyframes gradient-rotate {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
.tech-border-wrapper {
|
||||
position: relative;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#64748b,
|
||||
#8b5cf6,
|
||||
#06b6d4,
|
||||
#8b5cf6,
|
||||
#64748b
|
||||
);
|
||||
background-size: 300% 100%;
|
||||
animation: gradient-rotate 6s ease infinite;
|
||||
}
|
||||
|
||||
.tech-border-wrapper > * {
|
||||
border-radius: 0.65rem;
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import type { ChatMode } from '@/types/chat'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000'
|
||||
const USE_MOCK_DATA = true // Set to false when backend is ready
|
||||
|
||||
// Session management
|
||||
export const getChatSessionId = (): string | null => {
|
||||
return localStorage.getItem('tyndale_session_id')
|
||||
}
|
||||
|
||||
export const setChatSessionId = (sessionId: string): void => {
|
||||
localStorage.setItem('tyndale_session_id', sessionId)
|
||||
}
|
||||
|
||||
export const clearChatSessionId = (): void => {
|
||||
localStorage.removeItem('tyndale_session_id')
|
||||
}
|
||||
|
||||
// Mock responses for testing
|
||||
const MOCK_RESPONSES = {
|
||||
general: [
|
||||
"A **moving average** is one of the most fundamental technical indicators in trading. It smooths out price data by creating a constantly updated average price over a specific time period.\n\nThere are two main types:\n- **Simple Moving Average (SMA)**: The arithmetic mean of prices over a period\n- **Exponential Moving Average (EMA)**: Gives more weight to recent prices\n\nTraders use moving averages to identify trends, support/resistance levels, and generate trading signals when different MAs cross each other.",
|
||||
"**Algorithmic trading** uses computer programs to execute trades based on predefined criteria. The main advantages include:\n\n1. **Speed**: Algorithms can analyze market conditions and execute orders in milliseconds\n2. **Emotion-free**: Removes psychological biases from trading decisions\n3. **Backtesting**: Strategies can be tested on historical data before risking real capital\n4. **Consistency**: Executes trades exactly as programmed\n\nCommon strategies include market making, arbitrage, trend following, and mean reversion.",
|
||||
"**Risk management** is crucial in trading. Key principles include:\n\n- **Position sizing**: Never risk more than 1-2% of capital on a single trade\n- **Stop losses**: Set predetermined exit points to limit losses\n- **Diversification**: Spread risk across multiple assets or strategies\n- **Risk-reward ratio**: Aim for at least 2:1 reward-to-risk\n\nThe goal is to preserve capital during losing streaks while maximizing gains during winning periods.",
|
||||
"**Market volatility** refers to the rate and magnitude of price changes in a financial instrument. High volatility means larger price swings, while low volatility indicates stability.\n\nTraders measure volatility using:\n- **Standard deviation** of returns\n- **Average True Range (ATR)**\n- **Bollinger Bands**\n- **VIX index** (for equity markets)\n\nVolatility creates both opportunities and risks - more movement means more profit potential but also higher risk.",
|
||||
],
|
||||
strategy: [
|
||||
"When analyzing a **mean reversion strategy**, consider these key metrics:\n\n**Performance Metrics:**\n- Sharpe Ratio: Risk-adjusted returns (target > 1.5)\n- Maximum Drawdown: Largest peak-to-trough decline (keep < 20%)\n- Win Rate: Percentage of profitable trades\n- Profit Factor: Gross profit / Gross loss (target > 1.5)\n\n**Strategy-Specific:**\n- Mean reversion typically works best in range-bound markets\n- Watch for regime changes where markets shift from mean-reverting to trending\n- Consider transaction costs - they can significantly impact high-frequency mean reversion strategies",
|
||||
"**Backtesting results** should be interpreted carefully to avoid overfitting:\n\n**Red flags:**\n- Too many parameters (> 5-6 tunable variables)\n- Perfect or near-perfect equity curve\n- Performance degrades significantly on out-of-sample data\n- Very high Sharpe ratio (> 3) - often indicates curve fitting\n\n**Best practices:**\n- Use walk-forward analysis\n- Test on multiple market regimes\n- Include realistic transaction costs and slippage\n- Reserve 30-40% of data for out-of-sample testing\n\nRemember: Past performance does not guarantee future results.",
|
||||
"For **momentum strategies**, optimization should focus on:\n\n**Entry signals:**\n- Lookback period for momentum calculation (20-250 days typical)\n- Threshold for signal generation (% gain required)\n- Volume confirmation requirements\n\n**Risk management:**\n- Trailing stops to protect profits\n- Maximum holding period\n- Correlation filters to avoid overcrowding\n\n**Portfolio construction:**\n- Number of positions (5-20 for diversification)\n- Position sizing (equal weight vs volatility-adjusted)\n- Rebalancing frequency (weekly to monthly)\n\nMomentum works well in trending markets but can suffer during reversals.",
|
||||
"**Strategy robustness** can be evaluated through:\n\n1. **Parameter sensitivity**: Test nearby parameter values - robust strategies shouldn't degrade rapidly\n2. **Market regime analysis**: Performance across bull/bear/sideways markets\n3. **Asset class generalization**: Does it work on similar instruments?\n4. **Time frame stability**: Consistent performance across different time periods\n5. **Monte Carlo simulation**: Randomize trade sequences to assess statistical significance\n\nA robust strategy maintains profitability across reasonable parameter ranges and different market conditions.",
|
||||
],
|
||||
}
|
||||
|
||||
// Mock streaming function
|
||||
async function* mockChatStream(
|
||||
question: string,
|
||||
mode: ChatMode
|
||||
): AsyncGenerator<StreamEvent> {
|
||||
const sessionId = crypto.randomUUID()
|
||||
yield { type: 'session_id', data: sessionId }
|
||||
|
||||
// Select a random response based on mode
|
||||
const responses = MOCK_RESPONSES[mode]
|
||||
const response = responses[Math.floor(Math.random() * responses.length)]
|
||||
|
||||
// Simulate streaming by yielding words with delays
|
||||
const words = response.split(' ')
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
// Add space before word (except first word)
|
||||
const chunk = i === 0 ? words[i] : ' ' + words[i]
|
||||
yield { type: 'chunk', data: chunk }
|
||||
|
||||
// Random delay between 30-80ms to simulate typing
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 50 + 30))
|
||||
}
|
||||
|
||||
yield { type: 'done', data: null }
|
||||
}
|
||||
|
||||
// SSE stream event types
|
||||
export type StreamEvent =
|
||||
| { type: 'chunk'; data: string }
|
||||
| { type: 'done'; data: null }
|
||||
| { type: 'error'; data: string }
|
||||
| { type: 'session_id'; data: string }
|
||||
|
||||
class ApiClient {
|
||||
private sessionId: string | null = null
|
||||
|
||||
getSessionId(): string | null {
|
||||
return this.sessionId || getChatSessionId()
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
this.sessionId = sessionId
|
||||
setChatSessionId(sessionId)
|
||||
}
|
||||
|
||||
clearSessionId(): void {
|
||||
this.sessionId = null
|
||||
clearChatSessionId()
|
||||
}
|
||||
|
||||
// Streaming chat endpoint (SSE)
|
||||
async *chatStream(
|
||||
question: string,
|
||||
mode: ChatMode,
|
||||
sessionId?: string
|
||||
): AsyncGenerator<StreamEvent> {
|
||||
// Use mock data if enabled
|
||||
if (USE_MOCK_DATA) {
|
||||
yield* mockChatStream(question, mode)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate or reuse session ID
|
||||
const actualSessionId = sessionId || this.getSessionId() || crypto.randomUUID()
|
||||
this.setSessionId(actualSessionId)
|
||||
|
||||
// Yield session ID first
|
||||
yield { type: 'session_id', data: actualSessionId }
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/chat/stream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question,
|
||||
mode,
|
||||
sessionId: actualSessionId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
yield { type: 'error', data: `HTTP error! status: ${response.status}` }
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
yield { type: 'error', data: 'Response body is null' }
|
||||
return
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6)
|
||||
|
||||
if (data === '[DONE]') {
|
||||
yield { type: 'done', data: null }
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
if (parsed.type === 'error') {
|
||||
yield { type: 'error', data: parsed.message }
|
||||
} else if (parsed.content) {
|
||||
yield { type: 'chunk', data: parsed.content }
|
||||
} else {
|
||||
// Raw string chunk
|
||||
yield { type: 'chunk', data: data }
|
||||
}
|
||||
} catch {
|
||||
// Raw string chunk (not JSON)
|
||||
yield { type: 'chunk', data: data }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield { type: 'done', data: null }
|
||||
} catch (error) {
|
||||
yield {
|
||||
type: 'error',
|
||||
data: error instanceof Error ? error.message : 'Network error',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient()
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
import TyndaleBot from '@/components/chat/TyndaleBot'
|
||||
import TradingBackground from '@/components/chat/TradingBackground'
|
||||
|
||||
const ChatPage = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-trading-dark via-trading-blue to-trading-dark relative">
|
||||
<TradingBackground />
|
||||
<TyndaleBot />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatPage
|
||||
@@ -0,0 +1,15 @@
|
||||
import LandingNavigation from '@/components/landing/LandingNavigation'
|
||||
import LandingHero from '@/components/landing/LandingHero'
|
||||
import LandingFooter from '@/components/landing/LandingFooter'
|
||||
|
||||
const LandingPage = () => {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<LandingNavigation />
|
||||
<LandingHero />
|
||||
<LandingFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingPage
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
export type ChatMode = 'general' | 'strategy'
|
||||
|
||||
export interface Conversation {
|
||||
id: string
|
||||
title: string
|
||||
mode: ChatMode
|
||||
messages: Message[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
Reference in New Issue
Block a user