├── src ├── vite-env.d.ts ├── config │ └── messages.ts ├── components │ ├── ui │ │ ├── use-toast.ts │ │ ├── aspect-ratio.tsx │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── progress.tsx │ │ ├── toaster.tsx │ │ ├── sonner.tsx │ │ ├── checkbox.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── hover-card.tsx │ │ ├── popover.tsx │ │ ├── avatar.tsx │ │ ├── toggle.tsx │ │ ├── radio-group.tsx │ │ ├── alert.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── toggle-group.tsx │ │ ├── tabs.tsx │ │ ├── button.tsx │ │ ├── accordion.tsx │ │ ├── card.tsx │ │ ├── input-otp.tsx │ │ ├── calendar.tsx │ │ ├── breadcrumb.tsx │ │ ├── pagination.tsx │ │ ├── table.tsx │ │ ├── drawer.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── form.tsx │ │ ├── alert-dialog.tsx │ │ ├── toast.tsx │ │ ├── command.tsx │ │ ├── navigation-menu.tsx │ │ └── select.tsx │ ├── chat │ │ ├── KaTeXConfig.ts │ │ ├── CodeBlockRenderer.tsx │ │ ├── LaTeXBlock.tsx │ │ ├── ImageUpload.tsx │ │ ├── ChatMessages.tsx │ │ ├── StyleProvider.tsx │ │ ├── MarkdownRenderer.tsx │ │ ├── CodeBlock.tsx │ │ ├── ChatLayout.tsx │ │ └── ChatInput.tsx │ ├── playground │ │ ├── PlaygroundOutput.tsx │ │ ├── constants.ts │ │ └── EditorHeader.tsx │ ├── ThemeToggle.tsx │ ├── CodeBlock.tsx │ ├── ChatMessage.tsx │ └── CodePlayground.tsx ├── utils │ ├── uuid.ts │ ├── fileOperations.ts │ ├── fetchWithTimeout.ts │ ├── markdownConfig.ts │ ├── apiResponseHandler.ts │ ├── fileUpload.ts │ ├── responseHandler.ts │ └── codeExecutor.ts ├── main.tsx ├── lib │ └── utils.ts ├── types │ ├── window.d.ts │ └── chat.ts ├── hooks │ ├── use-mobile.tsx │ ├── useSpeechRecognition.ts │ ├── usePopoutWindow.ts │ ├── useMessageSender.ts │ ├── use-toast.ts │ └── useChatSessions.ts ├── App.css ├── pages │ ├── NotFound.tsx │ ├── Playground.tsx │ └── Index.tsx ├── App.tsx └── index.css ├── bun.lockb ├── .vscode └── extensions.json ├── public ├── favicon.ico ├── og-image.png ├── env-config.js └── placeholder.svg ├── .gitattributes ├── postcss.config.js ├── .env ├── .gitignore ├── tsconfig.json ├── components.json ├── docker-compose.yml ├── tsconfig.node.json ├── nginx.conf ├── vite.config.ts ├── tsconfig.app.json ├── eslint.config.js ├── .github └── workflows │ └── docker-image.yml ├── index.html ├── Dockerfile ├── tailwind.config.ts ├── README.md └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmartinquisitive/talkflow-n8n/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/config/messages.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_WELCOME_MESSAGE = "Welcome to the chat!."; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "sourcegraph.cody-ai" 4 | ] 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmartinquisitive/talkflow-n8n/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmartinquisitive/talkflow-n8n/HEAD/public/og-image.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .env merge=ours 2 | src/hooks/useChatSessions.ts merge=ours 3 | src/config/messages.ts merge=ours 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | import { useToast, toast } from "@/hooks/use-toast"; 2 | 3 | export { useToast, toast }; 4 | -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export const generateUUID = (): string => { 4 | return uuidv4(); 5 | }; -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_N8N_WEBHOOK_URL= 2 | VITE_WELCOME_MESSAGE= 3 | VITE_SITE_TITLE= 4 | VITE_ASSISTANT_NAME=Talkflow 5 | VITE_N8N_WEBHOOK_USERNAME= 6 | VITE_N8N_WEBHOOK_SECRET= 7 | -------------------------------------------------------------------------------- /public/env-config.js: -------------------------------------------------------------------------------- 1 | 2 | window.env = { 3 | VITE_N8N_WEBHOOK_URL: "", 4 | VITE_WELCOME_MESSAGE: "", 5 | VITE_SITE_TITLE: "", 6 | VITE_ASSISTANT_NAME: "" 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App.tsx' 3 | import './index.css' 4 | 5 | createRoot(document.getElementById("root")!).render(); 6 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } 6 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/types/window.d.ts: -------------------------------------------------------------------------------- 1 | 2 | interface Window { 3 | env?: { 4 | VITE_N8N_WEBHOOK_URL?: string; 5 | VITE_WELCOME_MESSAGE?: string; 6 | VITE_SITE_TITLE?: string; 7 | VITE_N8N_WEBHOOK_USERNAME?: string; 8 | VITE_N8N_WEBHOOK_SECRET?: string; 9 | VITE_ASSISTANT_NAME?: string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /src/types/chat.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: string; 3 | content: string; 4 | role: 'user' | 'assistant'; 5 | timestamp: number; 6 | imageData?: { 7 | data: string; 8 | mimeType: string; 9 | fileName: string; 10 | }; 11 | } 12 | 13 | export interface ChatSession { 14 | id: string; 15 | name?: string; 16 | messages: Message[]; 17 | createdAt: number; 18 | lastUpdated: number; 19 | favorite?: boolean; 20 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "noImplicitAny": false, 13 | "noUnusedParameters": false, 14 | "skipLibCheck": true, 15 | "allowJs": true, 16 | "noUnusedLocals": false, 17 | "strictNullChecks": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/chat/KaTeXConfig.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | import mk from 'markdown-it-katex'; 3 | 4 | export const createMarkdownRenderer = () => { 5 | const md = new MarkdownIt({ 6 | html: true, 7 | linkify: true, 8 | typographer: true, 9 | breaks: true 10 | }); 11 | 12 | // Configure KaTeX 13 | md.use(mk, { 14 | throwOnError: false, 15 | errorColor: '#cc0000', 16 | displayMode: true, 17 | strict: false 18 | }); 19 | 20 | return md; 21 | }; -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | ports: 8 | - "8009:80" 9 | environment: 10 | - VITE_N8N_WEBHOOK_URL=${VITE_N8N_WEBHOOK_URL} 11 | - VITE_WELCOME_MESSAGE=${VITE_WELCOME_MESSAGE} 12 | - VITE_SITE_TITLE=${VITE_SITE_TITLE} 13 | - VITE_N8N_WEBHOOK_USERNAME=${VITE_N8N_WEBHOOK_USERNAME} 14 | - VITE_N8N_WEBHOOK_SECRET=${VITE_N8N_WEBHOOK_SECRET} 15 | - VITE_ASSISTANT_NAME=${VITE_ASSISTANT_NAME} 16 | restart: unless-stopped 17 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/fileOperations.ts: -------------------------------------------------------------------------------- 1 | export const fileToBase64 = (file: File): Promise => { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | reader.readAsDataURL(file); 5 | reader.onload = () => { 6 | const base64String = reader.result as string; 7 | const base64Data = base64String.split(',')[1]; 8 | resolve(base64Data); 9 | }; 10 | reader.onerror = (error) => reject(error); 11 | }); 12 | }; 13 | 14 | export const prepareFileData = async (file: File) => { 15 | if (!file) return null; 16 | 17 | return { 18 | data: await fileToBase64(file), 19 | mimeType: file.type, 20 | fileName: file.name 21 | }; 22 | }; -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | 7 | # Handle SPA routing 8 | location / { 9 | try_files $uri $uri/ /index.html; 10 | add_header Cache-Control "no-store, no-cache, must-revalidate"; 11 | } 12 | 13 | # Specific no-cache for env-config.js 14 | location = /env-config.js { 15 | expires -1; 16 | add_header Cache-Control "no-store, no-cache, must-revalidate"; 17 | add_header Pragma "no-cache"; 18 | } 19 | 20 | # Cache control for static assets 21 | location /assets { 22 | expires 1y; 23 | add_header Cache-Control "public, no-transform"; 24 | } 25 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import path from "path"; 4 | import { componentTagger } from "lovable-tagger"; 5 | 6 | export default defineConfig(({ mode }) => ({ 7 | server: { 8 | host: "::", 9 | port: 8080, 10 | }, 11 | plugins: [ 12 | react(), 13 | mode === "development" && componentTagger(), 14 | ].filter(Boolean), 15 | resolve: { 16 | alias: { 17 | "@": path.resolve(__dirname, "./src"), 18 | }, 19 | }, 20 | optimizeDeps: { 21 | include: ["uuid"], // Ensures uuid is properly bundled 22 | }, 23 | build: { 24 | target: "esnext", // Ensure latest JS features are supported 25 | minify: false, // Disable minification for debugging 26 | }, 27 | })); 28 | -------------------------------------------------------------------------------- /src/components/playground/PlaygroundOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { executeHTML } from '@/utils/codeExecutor'; 3 | 4 | interface PlaygroundOutputProps { 5 | language: string; 6 | output: string; 7 | code: string; 8 | iframeRef: React.RefObject; 9 | outputRef: React.RefObject; 10 | } 11 | 12 | export const PlaygroundOutput: React.FC = ({ 13 | language, 14 | output, 15 | code, 16 | iframeRef, 17 | outputRef 18 | }) => { 19 | return language === 'html' ? ( 20 |
21 | ) : ( 22 |
23 | {output} 24 |
25 | ); 26 | }; -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noImplicitAny": false, 22 | "noFallthroughCasesInSwitch": false, 23 | 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "react-router-dom"; 2 | import { useEffect } from "react"; 3 | 4 | const NotFound = () => { 5 | const location = useLocation(); 6 | 7 | useEffect(() => { 8 | console.error( 9 | "404 Error: User attempted to access non-existent route:", 10 | location.pathname 11 | ); 12 | }, [location.pathname]); 13 | 14 | return ( 15 |
16 |
17 |

404

18 |

Oops! Page not found

19 | 20 | Return to Home 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default NotFound; 28 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |