| import { |
| endOfMonth, |
| startOfMonth, |
| startOfYear, |
| subDays, |
| subMonths, |
| } from "date-fns"; |
| import { create } from "zustand"; |
|
|
| |
| const COMMAND_SUGGESTIONS = [ |
| { |
| command: "/show", |
| title: "Show latest transactions", |
| toolName: "getTransactions", |
| toolParams: { pageSize: 10, sort: ["date", "desc"] }, |
| keywords: ["show", "latest", "transactions", "recent"], |
| }, |
| { |
| command: "/show", |
| title: "Show cash burn and top 3 vendor increases", |
| toolName: "getBurnRate", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "burn", "cash", "vendor", "increases", "analysis"], |
| }, |
| { |
| command: "/show", |
| title: "Show where we're spending the most this month", |
| toolName: "getSpending", |
| toolParams: { |
| from: startOfMonth(new Date()).toISOString(), |
| to: endOfMonth(new Date()).toISOString(), |
| showCanvas: true, |
| }, |
| keywords: ["show", "spending", "most", "this month", "where"], |
| }, |
| { |
| command: "/show", |
| title: "Show weekly trends and insights", |
| toolName: "getBurnRate", |
| toolParams: { |
| from: subDays(new Date(), 7).toISOString(), |
| to: new Date().toISOString(), |
| showCanvas: true, |
| }, |
| keywords: ["show", "weekly", "trends", "insights"], |
| }, |
| { |
| command: "/show", |
| title: "Show revenue performance", |
| toolName: "getRevenueSummary", |
| toolParams: { |
| from: startOfYear(new Date()).toISOString(), |
| to: endOfMonth(new Date()).toISOString(), |
| showCanvas: true, |
| }, |
| keywords: ["show", "revenue", "performance", "analyze"], |
| }, |
| { |
| command: "/show", |
| title: "Show expense breakdown by category", |
| toolName: "getSpending", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "expense", "breakdown", "category"], |
| }, |
| { |
| command: "/show", |
| title: "Show profit margins", |
| toolName: "getProfitAnalysis", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "profit", "margins"], |
| }, |
| { |
| command: "/show", |
| title: "Show cash runway", |
| toolName: "getRunway", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "runway", "cash", "left"], |
| }, |
| { |
| command: "/show", |
| title: "Show cash flow stress test", |
| toolName: "getCashFlowStressTest", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "stress", "test", "scenario", "resilience", "financial"], |
| }, |
| { |
| command: "/find", |
| title: "Find untagged transactions from last month", |
| toolName: "getTransactions", |
| toolParams: { |
| from: subMonths(new Date(), 1).toISOString(), |
| to: new Date().toISOString(), |
| statuses: ["pending"], |
| }, |
| keywords: ["find", "untagged", "transactions", "last month", "clean"], |
| }, |
| { |
| command: "/find", |
| title: "Find recurring payments", |
| toolName: "getTransactions", |
| toolParams: { recurring: true }, |
| keywords: ["find", "recurring", "payments", "subscriptions"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze burn rate trends", |
| toolName: "getBurnRate", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "burn", "rate", "trends"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze spending patterns", |
| toolName: "getSpending", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "spending", "patterns"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze financial resilience", |
| toolName: "getCashFlowStressTest", |
| toolParams: { showCanvas: true }, |
| keywords: [ |
| "analyze", |
| "stress", |
| "test", |
| "resilience", |
| "scenarios", |
| "financial", |
| ], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show balance sheet", |
| toolName: "getBalanceSheet", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "balance", "sheet", "assets", "liabilities", "equity"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show growth rate analysis", |
| toolName: "getGrowthRate", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "growth", "rate", "revenue", "profit", "trends"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze revenue growth trends", |
| toolName: "getGrowthRate", |
| toolParams: { showCanvas: true, type: "revenue" }, |
| keywords: ["analyze", "revenue", "growth", "trends", "period"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show invoice payment analysis", |
| toolName: "getInvoicePaymentAnalysis", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "invoice", "payment", "analysis", "days", "overdue"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze customer payment patterns", |
| toolName: "getInvoicePaymentAnalysis", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "customer", "payment", "patterns", "invoices"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show tax summary", |
| toolName: "getTaxSummary", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "tax", "summary", "deductions", "year"], |
| }, |
| { |
| command: "/show", |
| title: "Show tax breakdown by category", |
| toolName: "getTaxSummary", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "tax", "breakdown", "category", "deductions"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show business health score", |
| toolName: "getBusinessHealthScore", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "business", "health", "score", "metrics"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze business health metrics", |
| toolName: "getBusinessHealthScore", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "business", "health", "metrics", "performance"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show revenue forecast", |
| toolName: "getForecast", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "revenue", "forecast", "projection", "future"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze revenue projections", |
| toolName: "getForecast", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "revenue", "projections", "forecast", "trends"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show expenses breakdown", |
| toolName: "getExpensesBreakdown", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "expenses", "breakdown", "category", "analysis"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze expense categories", |
| toolName: "getExpensesBreakdown", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "expense", "categories", "breakdown"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show revenue summary", |
| toolName: "getRevenueSummary", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "revenue", "summary", "income", "earnings"], |
| }, |
| { |
| command: "/show", |
| title: "Show revenue trends this year", |
| toolName: "getRevenueSummary", |
| toolParams: { |
| from: startOfYear(new Date()).toISOString(), |
| to: endOfMonth(new Date()).toISOString(), |
| showCanvas: true, |
| }, |
| keywords: ["show", "revenue", "trends", "year", "this year"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show profit and loss statement", |
| toolName: "getProfitAnalysis", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "profit", "loss", "statement", "p&l"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze profit margins", |
| toolName: "getProfitAnalysis", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "profit", "margins", "profitability"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show account balances", |
| toolName: "getAccountBalances", |
| toolParams: {}, |
| keywords: ["show", "account", "balances", "bank", "accounts"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show latest invoices", |
| toolName: "getInvoices", |
| toolParams: { pageSize: 10, sort: ["createdAt", "desc"] }, |
| keywords: ["show", "latest", "invoices", "recent"], |
| }, |
| { |
| command: "/find", |
| title: "Find unpaid invoices", |
| toolName: "getInvoices", |
| toolParams: { statuses: ["unpaid"], pageSize: 20 }, |
| keywords: ["find", "unpaid", "invoices", "outstanding"], |
| }, |
| { |
| command: "/find", |
| title: "Find overdue invoices", |
| toolName: "getInvoices", |
| toolParams: { statuses: ["overdue"], pageSize: 20 }, |
| keywords: ["find", "overdue", "invoices", "late"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show customers", |
| toolName: "getCustomers", |
| toolParams: { pageSize: 10 }, |
| keywords: ["show", "customers", "clients", "list"], |
| }, |
| { |
| command: "/find", |
| title: "Find top customers", |
| toolName: "getCustomers", |
| toolParams: { pageSize: 10 }, |
| keywords: ["find", "top", "customers", "clients"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show cash flow", |
| toolName: "getCashFlow", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "cash", "flow", "income", "expenses"], |
| }, |
| { |
| command: "/show", |
| title: "Show cash flow this month", |
| toolName: "getCashFlow", |
| toolParams: { |
| from: startOfMonth(new Date()).toISOString(), |
| to: endOfMonth(new Date()).toISOString(), |
| showCanvas: true, |
| }, |
| keywords: ["show", "cash", "flow", "month", "this month"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze cash flow trends", |
| toolName: "getCashFlow", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "cash", "flow", "trends", "patterns"], |
| }, |
| |
| { |
| command: "/show", |
| title: "Show expenses", |
| toolName: "getExpenses", |
| toolParams: { showCanvas: true }, |
| keywords: ["show", "expenses", "costs", "spending"], |
| }, |
| { |
| command: "/show", |
| title: "Show expenses this month", |
| toolName: "getExpenses", |
| toolParams: { |
| from: startOfMonth(new Date()).toISOString(), |
| to: endOfMonth(new Date()).toISOString(), |
| showCanvas: true, |
| }, |
| keywords: ["show", "expenses", "month", "this month"], |
| }, |
| { |
| command: "/analyze", |
| title: "Analyze expense trends", |
| toolName: "getExpenses", |
| toolParams: { showCanvas: true }, |
| keywords: ["analyze", "expense", "trends", "patterns"], |
| }, |
| ]; |
|
|
| export interface CommandSuggestion { |
| command: string; |
| title: string; |
| toolName: string; |
| toolParams: Record<string, any>; |
| keywords: string[]; |
| } |
|
|
| interface ChatState { |
| |
| input: string; |
| setInput: (input: string) => void; |
| clearInput: () => void; |
|
|
| |
| isWebSearch: boolean; |
| setIsWebSearch: (isWebSearch: boolean) => void; |
|
|
| |
| isUploading: boolean; |
| setIsUploading: (isUploading: boolean) => void; |
|
|
| |
| isRecording: boolean; |
| isProcessing: boolean; |
| setIsRecording: (isRecording: boolean) => void; |
| setIsProcessing: (isProcessing: boolean) => void; |
|
|
| |
| showCommands: boolean; |
| setShowCommands: (showCommands: boolean) => void; |
| selectedCommandIndex: number; |
| setSelectedCommandIndex: (index: number) => void; |
| commandQuery: string; |
| setCommandQuery: (query: string) => void; |
| cursorPosition: number; |
| setCursorPosition: (position: number) => void; |
|
|
| |
| filteredCommands: CommandSuggestion[]; |
|
|
| |
| handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; |
| handleCommandSelect: (command: CommandSuggestion) => void; |
| handleKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void; |
| resetCommandState: () => void; |
| navigateCommandUp: () => void; |
| navigateCommandDown: () => void; |
| selectCurrentCommand: () => CommandSuggestion | null; |
| } |
|
|
| export const useChatStore = create<ChatState>()((set, get) => ({ |
| |
| input: "", |
| isWebSearch: false, |
| isUploading: false, |
| isRecording: false, |
| isProcessing: false, |
| showCommands: false, |
| selectedCommandIndex: 0, |
| commandQuery: "", |
| cursorPosition: 0, |
| filteredCommands: COMMAND_SUGGESTIONS, |
|
|
| |
| setInput: (input) => set({ input }), |
| clearInput: () => set({ input: "", cursorPosition: 0 }), |
| setIsWebSearch: (isWebSearch) => set({ isWebSearch }), |
| setIsUploading: (isUploading) => set({ isUploading }), |
| setIsRecording: (isRecording) => set({ isRecording }), |
| setIsProcessing: (isProcessing) => set({ isProcessing }), |
| setShowCommands: (showCommands) => set({ showCommands }), |
| setSelectedCommandIndex: (selectedCommandIndex) => |
| set({ selectedCommandIndex }), |
| setCommandQuery: (commandQuery) => set({ commandQuery }), |
| setCursorPosition: (cursorPosition) => set({ cursorPosition }), |
|
|
| |
| handleInputChange: (e) => { |
| const value = e.target.value; |
| const cursorPos = e.target.selectionStart; |
|
|
| set({ input: value, cursorPosition: cursorPos }); |
|
|
| |
| const textBeforeCursor = value.substring(0, cursorPos); |
| const lastSlashIndex = textBeforeCursor.lastIndexOf("/"); |
|
|
| if (lastSlashIndex !== -1) { |
| const textAfterSlash = textBeforeCursor.substring(lastSlashIndex + 1); |
|
|
| |
| const query = textAfterSlash.toLowerCase().trim(); |
| const filtered = COMMAND_SUGGESTIONS.filter((command) => { |
| const matchesCommand = command.command.toLowerCase().includes(query); |
| const matchesTitle = command.title.toLowerCase().includes(query); |
| const matchesKeywords = command.keywords.some((keyword) => |
| keyword.toLowerCase().includes(query), |
| ); |
| return matchesCommand || matchesTitle || matchesKeywords; |
| }); |
|
|
| |
| set({ |
| commandQuery: textAfterSlash, |
| showCommands: true, |
| selectedCommandIndex: 0, |
| filteredCommands: filtered, |
| }); |
| return; |
| } |
|
|
| set({ |
| showCommands: false, |
| commandQuery: "", |
| filteredCommands: COMMAND_SUGGESTIONS, |
| }); |
| }, |
|
|
| |
| handleCommandSelect: (command) => { |
| const { input, cursorPosition } = get(); |
| const textBeforeCursor = input.substring(0, cursorPosition); |
| const lastSlashIndex = textBeforeCursor.lastIndexOf("/"); |
| const textAfterCursor = input.substring(cursorPosition); |
|
|
| |
| const newText = `${textBeforeCursor.substring(0, lastSlashIndex)}${command.title} ${textAfterCursor}`; |
|
|
| set({ |
| input: newText, |
| showCommands: false, |
| commandQuery: "", |
| }); |
| }, |
|
|
| |
| handleKeyDown: (e) => { |
| const { showCommands, filteredCommands, selectedCommandIndex } = get(); |
|
|
| if (!showCommands) return; |
|
|
| switch (e.key) { |
| case "ArrowDown": |
| e.preventDefault(); |
| set({ |
| selectedCommandIndex: Math.min( |
| selectedCommandIndex + 1, |
| filteredCommands.length - 1, |
| ), |
| }); |
| break; |
| case "ArrowUp": |
| e.preventDefault(); |
| set({ |
| selectedCommandIndex: Math.max(selectedCommandIndex - 1, 0), |
| }); |
| break; |
| case "Enter": { |
| e.preventDefault(); |
| const currentCommand = get().selectCurrentCommand(); |
| if (currentCommand) { |
| get().handleCommandSelect(currentCommand); |
| } |
| break; |
| } |
| case "Escape": |
| set({ showCommands: false, commandQuery: "" }); |
| break; |
| } |
| }, |
|
|
| |
| resetCommandState: () => { |
| set({ |
| showCommands: false, |
| commandQuery: "", |
| selectedCommandIndex: 0, |
| }); |
| }, |
|
|
| navigateCommandUp: () => { |
| const { selectedCommandIndex, filteredCommands } = get(); |
| set({ |
| selectedCommandIndex: Math.max(selectedCommandIndex - 1, 0), |
| }); |
| }, |
|
|
| navigateCommandDown: () => { |
| const { selectedCommandIndex, filteredCommands } = get(); |
| set({ |
| selectedCommandIndex: Math.min( |
| selectedCommandIndex + 1, |
| filteredCommands.length - 1, |
| ), |
| }); |
| }, |
|
|
| selectCurrentCommand: () => { |
| const { filteredCommands, selectedCommandIndex } = get(); |
| return filteredCommands[selectedCommandIndex] || null; |
| }, |
| })); |
|
|