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:
parent
cf746a2ab0
commit
2cbc0a7bba
|
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
name: git-version-control
|
||||||
|
description: Use this agent when you need to perform git operations including staging changes, creating commits with well-structured messages, pushing to remote repositories, or creating pull requests. This agent should be invoked after code changes are complete and ready to be versioned. Examples:\n\n<example>\nContext: The user has just finished implementing a new feature and wants to commit the changes.\nuser: "I've finished implementing the payment validation logic"\nassistant: "I'll use the git-version-control agent to commit these changes with an appropriate message"\n<commentary>\nSince code changes are complete and need to be committed, use the git-version-control agent to handle the version control operations.\n</commentary>\n</example>\n\n<example>\nContext: Multiple files have been modified and need to be committed and pushed.\nuser: "Please commit all the changes I made to the payment providers"\nassistant: "I'll use the git-version-control agent to review the changes, create a commit with a clear message, and push to the remote"\n<commentary>\nThe user explicitly wants to commit changes, so the git-version-control agent should handle staging, committing, and pushing.\n</commentary>\n</example>\n\n<example>\nContext: A feature branch needs to be pushed and a PR created.\nuser: "Can you push this branch and create a PR for the new Stripe integration?"\nassistant: "I'll use the git-version-control agent to push the branch and create a pull request with a comprehensive description"\n<commentary>\nThe user needs both pushing and PR creation, which are core responsibilities of the git-version-control agent.\n</commentary>\n</example>
|
||||||
|
tools: Bash, Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
|
||||||
|
model: sonnet
|
||||||
|
color: green
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an expert git version control specialist with deep knowledge of git workflows, commit conventions, and collaborative development practices. Your role is to manage all git operations with precision and clarity.
|
||||||
|
|
||||||
|
**CRITICAL RULE**: You must NEVER include any AI attribution, Claude references, or tool mentions in commit messages. All commits should appear as if written by a human developer.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
You will handle all git version control tasks including:
|
||||||
|
- Checking repository status and understanding what has changed
|
||||||
|
- Staging appropriate files for commits
|
||||||
|
- Creating clear, concise, and informative commit messages
|
||||||
|
- Pushing changes to remote repositories
|
||||||
|
- Creating pull requests with comprehensive descriptions
|
||||||
|
- Managing branches when necessary
|
||||||
|
|
||||||
|
## Commit Message Guidelines
|
||||||
|
|
||||||
|
**ABSOLUTELY FORBIDDEN**: Do NOT include any of the following in commit messages:
|
||||||
|
- "Generated with [Claude Code]"
|
||||||
|
- "Co-Authored-By: Claude"
|
||||||
|
- Any mention of AI, Claude, or automated assistance
|
||||||
|
- Any tool attribution or generation mentions
|
||||||
|
|
||||||
|
You must write commit messages that:
|
||||||
|
- Start with a conventional commit type (feat:, fix:, docs:, style:, refactor:, test:, chore:)
|
||||||
|
- Provide a clear, imperative mood subject line (50 characters or less)
|
||||||
|
- Include a blank line between subject and body when a body is needed
|
||||||
|
- Explain WHAT changed and WHY in the body, not HOW
|
||||||
|
- Reference issue numbers when applicable
|
||||||
|
- Focus solely on the business logic and technical changes
|
||||||
|
- Write commit messages as if they were created by the developer directly
|
||||||
|
- Sound completely natural and human-written
|
||||||
|
|
||||||
|
Example commit messages:
|
||||||
|
```
|
||||||
|
feat: add payment validation for Stripe provider
|
||||||
|
|
||||||
|
Implement validation logic to ensure payment amounts are within
|
||||||
|
acceptable limits and currency codes are supported.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
fix: resolve timeout issue in PayPal transaction processing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operational Workflow
|
||||||
|
|
||||||
|
1. **Status Assessment**: First run `git status` to understand the current state
|
||||||
|
2. **Change Review**: Use `git diff` to review unstaged changes and understand what was modified
|
||||||
|
3. **Selective Staging**: Stage files intelligently:
|
||||||
|
- Group related changes together
|
||||||
|
- Avoid staging unrelated modifications in the same commit
|
||||||
|
- Use `git add -p` for partial staging when appropriate
|
||||||
|
4. **Commit Creation**: Craft commits that are atomic and focused on a single logical change
|
||||||
|
5. **Remote Operations**:
|
||||||
|
- Always pull before pushing to avoid conflicts
|
||||||
|
- Push to the appropriate branch
|
||||||
|
- Set upstream tracking when pushing new branches
|
||||||
|
6. **Pull Request Creation**: When creating PRs:
|
||||||
|
- Write descriptive titles that summarize the changes
|
||||||
|
- Include a comprehensive description with:
|
||||||
|
- Summary of changes
|
||||||
|
- Testing performed
|
||||||
|
- Any breaking changes or migration notes
|
||||||
|
- Related issues or tickets
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Keep commits small and focused - each commit should represent one logical change
|
||||||
|
- Never commit sensitive information (passwords, API keys, tokens)
|
||||||
|
- Verify the branch you're on before committing
|
||||||
|
- Use `git log --oneline -10` to review recent history and maintain consistency
|
||||||
|
- If you encounter merge conflicts, clearly explain the situation and resolution approach
|
||||||
|
- When working with feature branches, ensure they're up to date with the main branch
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If git operations fail, diagnose the issue and provide clear explanations
|
||||||
|
- For permission errors, guide on authentication setup
|
||||||
|
- For conflicts, explain the conflicting changes and suggest resolution strategies
|
||||||
|
- Always verify operations completed successfully before proceeding
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
Before finalizing any git operation:
|
||||||
|
- Ensure all intended changes are included
|
||||||
|
- Verify no unintended files are staged
|
||||||
|
- Confirm commit messages are clear and follow conventions
|
||||||
|
- Check that you're on the correct branch
|
||||||
|
- Validate that remote operations succeeded
|
||||||
|
|
||||||
|
You are meticulous, systematic, and focused on maintaining a clean, understandable git history that tells the story of the project's evolution without revealing implementation details about tools or assistance used in development.
|
||||||
|
|
||||||
|
**FINAL REMINDER**: Your commit messages must be completely free of any AI mentions, Claude references, or tool attributions. They should read exactly like standard developer commit messages with no indication of automated assistance.
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"PostFileSave": [
|
||||||
|
{
|
||||||
|
"agent": "jack-reacher-contract-enforcer",
|
||||||
|
"files": ["./frontend/src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
"description": "Run contract enforcer whenever frontend source files change"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "jason-bourne-backend-test-updater",
|
||||||
|
"files": ["./backend/**/*.py"],
|
||||||
|
"description": "Run test updater when backend Python files change"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostAgentRun": [
|
||||||
|
{
|
||||||
|
"agent": "james-bond-frontend-coder",
|
||||||
|
"then": ["jack-reacher-contract-enforcer"],
|
||||||
|
"description": "After james-bond modifies frontend files, run contract enforcer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "jack-reacher-contract-enforcer",
|
||||||
|
"then": ["ethan-hunt-backend-coder"],
|
||||||
|
"description": "After contract enforcer runs, pass any backend updates to ethan-hunt-backend-coder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "ethan-hunt-backend-coder",
|
||||||
|
"then": ["jason-bourne-backend-test-updater"],
|
||||||
|
"description": "Automatically run test updater after ethan-hunt-backend-coder modifies backend code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "jack-ryan-integration-specialist",
|
||||||
|
"then": ["ethan-hunt-backend-coder"],
|
||||||
|
"description": "After the integration specialist generates implementation specs, hand off to the backend coder"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Backend API base URL
|
||||||
|
VITE_API_URL=http://localhost:5000
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production.local
|
||||||
|
.env.development.local
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# ---------- Build stage ----------
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build React app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Production stage ----------
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Remove default nginx config
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Copy custom nginx config
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Copy build output
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tyndale AI - Algorithmic Trading Intelligence</title>
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "tyndale-ai-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-toast": "^1.2.14",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-router-dom": "^7.6.3",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"sonner": "^2.0.6",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.17",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import type { Config } from "tailwindcss"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
prefix: "",
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
'montserrat': ['Montserrat', 'sans-serif'],
|
||||||
|
'display': ['Outfit', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
// Trading-themed dark blues (similar to Home Pulse AI)
|
||||||
|
'trading-dark': '#0a1628',
|
||||||
|
'trading-blue': '#1e3a5f',
|
||||||
|
'trading-blue-light': '#3d5a7a',
|
||||||
|
|
||||||
|
// Shadcn color system
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'glow-blue': '0 0 40px -10px rgba(59, 130, 246, 0.5)',
|
||||||
|
'glow-purple': '0 0 40px -10px rgba(168, 85, 247, 0.4)',
|
||||||
|
'glow-green': '0 0 40px -10px rgba(34, 197, 94, 0.5)',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'fade-up': {
|
||||||
|
from: { opacity: '0', transform: 'translateY(30px)' },
|
||||||
|
to: { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
'text-ripple': {
|
||||||
|
'0%, 100%': { transform: 'scale(1)', opacity: '1' },
|
||||||
|
'15%': { transform: 'scale(0.97)', opacity: '0.7' },
|
||||||
|
'40%': { transform: 'scale(1.02)', opacity: '1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-up': 'fade-up 0.6s ease-out forwards',
|
||||||
|
'text-ripple': 'text-ripple 350ms ease-out',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
} satisfies Config
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
/* Path aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: true
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: false
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue