From a25d10a5282a6a1451c88f4f2290ad9eacf70582 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Fri, 13 Feb 2026 14:00:46 +0100 Subject: [PATCH] Initial copy from influencer-biz --- .claude/skills/langchain/SKILL.md | 480 + .claude/skills/langchain/references/agents.md | 499 + .../langchain/references/integration.md | 562 + .claude/skills/langchain/references/rag.md | 600 + .gitignore | 41 + README.md | 36 + app/(auth)/auth/callback/route.ts | 35 + app/(auth)/login/page.tsx | 144 + app/(dashboard)/analytics/page.tsx | 333 + app/(dashboard)/bookkeeping/page.tsx | 42 + app/(dashboard)/calendar/page.tsx | 142 + app/(dashboard)/campaigns/[id]/page.tsx | 103 + app/(dashboard)/campaigns/import/page.tsx | 40 + app/(dashboard)/campaigns/new/page.tsx | 351 + app/(dashboard)/campaigns/page.tsx | 85 + app/(dashboard)/customers/[id]/page.tsx | 445 + app/(dashboard)/customers/page.tsx | 217 + app/(dashboard)/deductions/page.tsx | 379 + app/(dashboard)/gifts/page.tsx | 366 + app/(dashboard)/help/page.tsx | 428 + app/(dashboard)/import/page.tsx | 371 + app/(dashboard)/invoices/[id]/credit/page.tsx | 316 + app/(dashboard)/invoices/[id]/page.tsx | 736 + app/(dashboard)/invoices/new/page.tsx | 474 + app/(dashboard)/invoices/page.tsx | 292 + app/(dashboard)/layout.tsx | 53 + app/(dashboard)/page.tsx | 407 + app/(dashboard)/receipts/page.tsx | 369 + app/(dashboard)/receipts/scan/page.tsx | 157 + app/(dashboard)/reports/page.tsx | 1230 ++ app/(dashboard)/settings/page.tsx | 830 ++ app/(dashboard)/shadow-ledger/new/page.tsx | 140 + app/(dashboard)/shadow-ledger/page.tsx | 237 + app/(dashboard)/transactions/page.tsx | 687 + app/(onboarding)/onboarding/page.tsx | 420 + app/(public)/invoice-action/[token]/page.tsx | 249 + app/api/banking/callback/route.ts | 151 + app/api/banking/connect/route.ts | 92 + app/api/banking/sync/cron/route.ts | 176 + app/api/banking/sync/route.ts | 86 + .../bookkeeping/accounts/[number]/route.ts | 36 + app/api/bookkeeping/accounts/route.ts | 72 + app/api/bookkeeping/fiscal-periods/route.ts | 52 + .../journal-entries/[id]/reverse/route.ts | 26 + .../bookkeeping/journal-entries/[id]/route.ts | 28 + app/api/bookkeeping/journal-entries/route.ts | 74 + .../mapping-rules/evaluate/route.ts | 45 + app/api/bookkeeping/mapping-rules/route.ts | 64 + app/api/briefings/[id]/download/route.ts | 65 + app/api/briefings/[id]/route.ts | 159 + app/api/briefings/summarize/route.ts | 57 + app/api/calendar/feed/[token]/route.ts | 136 + app/api/calendar/feed/route.ts | 178 + app/api/campaigns/[id]/briefings/route.ts | 146 + .../campaigns/[id]/briefings/upload/route.ts | 117 + app/api/campaigns/[id]/contracts/route.ts | 185 + app/api/campaigns/[id]/deliverables/route.ts | 126 + app/api/campaigns/[id]/exclusivities/route.ts | 159 + app/api/campaigns/[id]/route.ts | 204 + app/api/campaigns/from-contract/route.ts | 321 + app/api/campaigns/route.ts | 173 + app/api/campaigns/workload/route.ts | 144 + app/api/chat/route.ts | 169 + app/api/chat/sessions/[id]/route.ts | 146 + app/api/chat/sessions/route.ts | 84 + app/api/chat/stream/route.ts | 195 + app/api/contracts/[id]/download/route.ts | 55 + app/api/contracts/[id]/extract/route.ts | 244 + app/api/contracts/[id]/route.ts | 163 + app/api/customers/[id]/route.ts | 133 + app/api/customers/route.ts | 64 + app/api/deadlines/[id]/complete/route.ts | 56 + app/api/deadlines/[id]/route.ts | 133 + app/api/deadlines/[id]/status/route.ts | 105 + app/api/deadlines/route.ts | 109 + app/api/deadlines/status/cron/route.ts | 55 + app/api/deliverables/[id]/route.ts | 153 + app/api/deliverables/[id]/status/route.ts | 126 + app/api/exclusivities/[id]/route.ts | 145 + app/api/exclusivities/conflicts/route.ts | 102 + app/api/gifts/[id]/route.ts | 168 + app/api/gifts/estimate/route.ts | 51 + app/api/gifts/route.ts | 170 + app/api/gifts/summary/route.ts | 81 + app/api/import/sie/[id]/route.ts | 71 + app/api/import/sie/create-accounts/route.ts | 150 + app/api/import/sie/execute/route.ts | 130 + app/api/import/sie/mappings/route.ts | 155 + app/api/import/sie/parse/route.ts | 139 + app/api/import/sie/route.ts | 48 + app/api/invoices/[id]/pdf/route.ts | 100 + app/api/invoices/[id]/send/route.ts | 169 + app/api/invoices/reminders/action/route.ts | 173 + app/api/invoices/reminders/cron/route.ts | 73 + app/api/invoices/route.ts | 322 + app/api/push/cron/route.ts | 81 + app/api/push/subscribe/route.ts | 136 + app/api/receipts/[id]/confirm/route.ts | 108 + app/api/receipts/[id]/match/route.ts | 218 + app/api/receipts/[id]/route.ts | 176 + app/api/receipts/product/route.ts | 156 + app/api/receipts/queue/route.ts | 106 + app/api/receipts/route.ts | 55 + app/api/receipts/upload/route.ts | 190 + app/api/reports/balance-sheet/route.ts | 44 + app/api/reports/income-statement/route.ts | 45 + app/api/reports/ne-declaration/route.ts | 69 + app/api/reports/sie-export/route.ts | 53 + app/api/reports/trial-balance/route.ts | 29 + app/api/reports/vat-declaration/route.ts | 114 + app/api/settings/route.ts | 75 + app/api/shadow-ledger/[id]/route.ts | 85 + app/api/shadow-ledger/route.ts | 121 + app/api/shadow-ledger/summary/route.ts | 85 + app/api/tax-deadlines/cron/route.ts | 51 + app/api/tax-deadlines/generate/route.ts | 54 + app/api/tiktok/accounts/route.ts | 49 + app/api/tiktok/callback/route.ts | 117 + app/api/tiktok/connect/route.ts | 47 + app/api/tiktok/cron/route.ts | 62 + app/api/tiktok/disconnect/route.ts | 61 + app/api/tiktok/stats/route.ts | 60 + app/api/tiktok/sync/route.ts | 78 + app/api/tiktok/videos/[id]/link/route.ts | 153 + app/api/tiktok/videos/route.ts | 63 + app/api/transactions/[id]/categorize/route.ts | 236 + .../transactions/[id]/match-invoice/route.ts | 220 + .../batch-match-invoices/route.ts | 62 + .../transactions/suggest-categories/route.ts | 76 + app/api/vat/validate/route.ts | 128 + app/favicon.ico | Bin 0 -> 25931 bytes app/globals.css | 286 + app/layout.tsx | 61 + app/page.tsx | 260 + components/banking/BankConnectionStatus.tsx | 167 + components/banking/BankSelector.tsx | 102 + components/banking/index.ts | 2 + components/benefits/GiftForm.tsx | 290 + components/benefits/GiftList.tsx | 198 + components/benefits/GiftSummaryCard.tsx | 55 + components/bookkeeping/ChartOfAccounts.tsx | 166 + components/bookkeeping/JournalEntryForm.tsx | 288 + components/bookkeeping/JournalEntryList.tsx | 213 + components/calendar/CalendarDayCell.tsx | 165 + components/calendar/CalendarDayView.tsx | 245 + components/calendar/CalendarGrid.tsx | 55 + components/calendar/CalendarHeader.tsx | 85 + components/calendar/CalendarWeekView.tsx | 157 + components/calendar/DayDetailModal.tsx | 240 + components/calendar/DeadlineCard.tsx | 175 + components/calendar/DeadlineFilters.tsx | 67 + components/calendar/DeadlineForm.tsx | 269 + components/calendar/DeadlineList.tsx | 217 + components/calendar/PaymentCalendar.tsx | 200 + components/calendar/PaymentSummaryCard.tsx | 98 + components/calendar/TaxTodoWidget.tsx | 243 + .../calendar/UpcomingDeadlinesWidget.tsx | 213 + components/calendar/ViewModeSelector.tsx | 34 + components/calendar/index.ts | 12 + components/campaigns/BriefingForm.tsx | 478 + components/campaigns/BriefingList.tsx | 359 + components/campaigns/CampaignCard.tsx | 175 + components/campaigns/CampaignDetail.tsx | 514 + components/campaigns/CampaignForm.tsx | 392 + .../campaigns/CampaignInvoiceSummary.tsx | 195 + components/campaigns/CampaignList.tsx | 201 + components/campaigns/CampaignStatusBadge.tsx | 37 + components/campaigns/CampaignsWidget.tsx | 159 + components/campaigns/ContractList.tsx | 326 + components/campaigns/ContractUpload.tsx | 214 + components/campaigns/DeliverableCard.tsx | 247 + components/campaigns/DeliverableForm.tsx | 251 + components/campaigns/DeliverableList.tsx | 190 + components/campaigns/ExclusivityForm.tsx | 381 + components/campaigns/ExclusivityList.tsx | 202 + components/campaigns/index.ts | 14 + components/chat/ChatInput.tsx | 83 + components/chat/ChatMessage.tsx | 116 + components/chat/ChatPanel.tsx | 159 + components/chat/ChatWidget.tsx | 55 + components/chat/index.ts | 5 + components/chat/useChatStream.ts | 215 + components/contracts/ConfidenceIndicator.tsx | 100 + components/contracts/ContractImportWizard.tsx | 1356 ++ .../contracts/ExtractionStatusBadge.tsx | 68 + components/customers/CustomerForm.tsx | 321 + components/dashboard/DashboardContent.tsx | 532 + components/dashboard/DashboardNav.tsx | 453 + components/dashboard/FSkattWarningCard.tsx | 172 + components/dashboard/GiftTaxDebtCard.tsx | 79 + components/dashboard/IncomeChart.tsx | 139 + .../dashboard/LightDashboardContent.tsx | 134 + components/dashboard/RecentPayoutsCard.tsx | 94 + components/dashboard/SGIShieldWidget.tsx | 107 + components/dashboard/SafeToSpendGauge.tsx | 131 + components/import/AccountMappingStep.tsx | 376 + components/import/ImportResultStep.tsx | 212 + components/import/ImportReviewStep.tsx | 269 + components/import/SIEPreviewStep.tsx | 319 + components/import/SIEUploadStep.tsx | 175 + components/onboarding/DashboardTour.tsx | 258 + components/onboarding/NewUserChecklist.tsx | 217 + components/onboarding/Step1EntityType.tsx | 137 + components/onboarding/Step2CompanyDetails.tsx | 171 + .../onboarding/Step2LightPersonalInfo.tsx | 100 + .../onboarding/Step3LightTaxProfile.tsx | 526 + .../onboarding/Step3TaxRegistration.tsx | 280 + components/onboarding/Step4PreliminaryTax.tsx | 140 + components/onboarding/Step5BankDetails.tsx | 198 + components/onboarding/Step6ConnectBank.tsx | 273 + components/push/PushPrompt.tsx | 211 + components/receipts/ProductCapture.tsx | 473 + components/receipts/ReceiptCamera.tsx | 336 + components/receipts/ReceiptDashboard.tsx | 179 + components/receipts/ReceiptLineItemRow.tsx | 99 + components/receipts/ReceiptReviewView.tsx | 418 + components/receipts/TransactionMatcher.tsx | 229 + components/receipts/index.ts | 6 + components/settings/CalendarFeedSettings.tsx | 372 + components/settings/NotificationSettings.tsx | 366 + .../settings/SchablonavdragSettings.tsx | 204 + components/shadow-ledger/PayoutWaterfall.tsx | 139 + components/shadow-ledger/ShadowLedgerForm.tsx | 388 + components/shadow-ledger/ShadowLedgerList.tsx | 199 + components/tiktok/TikTokAccountCard.tsx | 225 + components/tiktok/TikTokConnectButton.tsx | 84 + components/tiktok/TikTokGrowthChart.tsx | 174 + components/tiktok/TikTokROITable.tsx | 142 + components/tiktok/TikTokStatsWidget.tsx | 162 + components/tiktok/TikTokVideoCard.tsx | 132 + components/tiktok/TikTokVideoList.tsx | 117 + components/tiktok/VideoLinkModal.tsx | 249 + components/tiktok/index.ts | 8 + components/transactions/MileageEntry.tsx | 242 + .../transactions/SwipeCategorizationView.tsx | 479 + components/transactions/TransactionForm.tsx | 201 + components/ui/badge.tsx | 40 + components/ui/button.tsx | 60 + components/ui/card.tsx | 78 + components/ui/checkbox.tsx | 29 + components/ui/dialog.tsx | 121 + components/ui/empty-state.tsx | 206 + components/ui/info-tooltip.tsx | 162 + components/ui/input.tsx | 24 + components/ui/label.tsx | 25 + components/ui/page-header.tsx | 21 + components/ui/progress.tsx | 27 + components/ui/segmented-progress.tsx | 133 + components/ui/select.tsx | 158 + components/ui/separator.tsx | 31 + components/ui/skeleton.tsx | 15 + components/ui/success-animation.tsx | 81 + components/ui/switch.tsx | 28 + components/ui/table.tsx | 116 + components/ui/tabs.tsx | 54 + components/ui/textarea.tsx | 23 + components/ui/toast.tsx | 130 + components/ui/toaster.tsx | 35 + components/ui/use-toast.tsx | 187 + dev_docs/01-PRD.md | 137 + dev_docs/02-ARCHITECTURE.md | 343 + dev_docs/03-DATABASE-SCHEMA.md | 688 + dev_docs/04-API-SPECIFICATION.md | 672 + dev_docs/05-UI-SPECIFICATION.md | 728 + dev_docs/06-IMPLEMENTATION-GUIDE.md | 1157 ++ dev_docs/07-FUTURE-FEATURES.md | 519 + dev_docs/08-BAS-ACCOUNTING-GUIDE.md | 903 ++ dev_docs/ALICE_DATA_02_10.json | 721 + dev_docs/ENABLE_BANKING_DOCS/API_EB.md | 940 ++ .../BUSINESS_BANKING_SETUP.md | 224 + dev_docs/ENABLE_BANKING_DOCS/EB_UI_WIDGETS.md | 148 + dev_docs/ENABLE_BANKING_DOCS/QUICKSTART_EB.md | 198 + .../ENABLE_BANKING_DOCS/QUICKSTART_EB_GK.md | 210 + dev_docs/FEEDBACK_02_09.md | 357 + dev_docs/FULL_BAS.md | 2192 +++ dev_docs/K1_BAS.md | 469 + dev_docs/NYA_FEATURES.md | 41 + dev_docs/README.md | 186 + dev_docs/archive/alice_bank_data.json | 506 + .../influencer_ai_info/00-ai-snabbreferens.md | 234 + dev_docs/influencer_ai_info/01-karnregler.md | 219 + .../02-scenariobibliotek.md | 678 + .../03-plattformskatalog.md | 398 + .../04-berakningsexempel.md | 601 + dev_docs/influencer_ai_info/05-beslutstrад.md | 554 + .../influencer_ai_info/06-felsokningsguide.md | 500 + .../07-kalender-deadlines.md | 306 + .../influencer_ai_info/08-varningsflaggor.md | 542 + eslint.config.mjs | 18 + lib/ai/chatbot/chain.ts | 137 + lib/ai/chatbot/config.ts | 25 + lib/ai/chatbot/embeddings.ts | 25 + lib/ai/chatbot/prompts.ts | 79 + lib/ai/chatbot/retriever.ts | 51 + lib/ai/ingestion/ingest.ts | 345 + lib/banking/enable-banking.ts | 472 + lib/banking/jwt.ts | 91 + lib/banking/sync-transactions.ts | 143 + lib/benefits/gift-booking.ts | 219 + lib/benefits/gift-classifier.ts | 291 + lib/bookkeeping/category-mapping.ts | 233 + lib/bookkeeping/engine.ts | 263 + lib/bookkeeping/invoice-entries.ts | 226 + lib/bookkeeping/mapping-engine.ts | 229 + lib/bookkeeping/transaction-entries.ts | 201 + lib/bookkeeping/vat-entries.ts | 158 + lib/calendar/ics-generator.ts | 319 + lib/calendar/utils.ts | 668 + lib/campaigns/deadline-generator.ts | 267 + lib/campaigns/exclusivity-checker.ts | 212 + lib/campaigns/index.ts | 4 + lib/campaigns/payment-tracker.ts | 252 + lib/campaigns/workload-analyzer.ts | 287 + lib/contracts/contract-analyzer.ts | 424 + lib/contracts/pdf-extractor.ts | 33 + lib/currency/riksbanken.ts | 150 + lib/customers/customer-matcher.ts | 231 + lib/deadlines/status-engine.ts | 229 + lib/email/invoice-templates.ts | 205 + lib/email/reminder-templates.ts | 280 + lib/email/resend.ts | 116 + lib/import/account-mapper.ts | 365 + lib/import/sie-import.ts | 675 + lib/import/sie-parser.ts | 643 + lib/import/types.ts | 308 + lib/invoice/invoice-matching.ts | 212 + lib/invoice/pdf-template.tsx | 465 + lib/invoice/vat-rules.ts | 115 + lib/invoices/reminder-processor.ts | 270 + lib/light/dashboard-data.ts | 193 + lib/push/notification-scheduler.ts | 488 + lib/push/web-push.ts | 232 + lib/receipts/receipt-analyzer.ts | 460 + lib/receipts/receipt-categorizer.ts | 301 + lib/receipts/receipt-matcher.ts | 288 + lib/receipts/receipt-utils.ts | 94 + lib/reports/balance-sheet.ts | 104 + lib/reports/income-statement.ts | 160 + lib/reports/ne-declaration.ts | 415 + lib/reports/sie-export.ts | 173 + lib/reports/sru-generator.ts | 209 + lib/reports/trial-balance.ts | 145 + lib/reports/vat-declaration.ts | 423 + lib/supabase/client.ts | 8 + lib/supabase/middleware.ts | 108 + lib/supabase/server.ts | 54 + lib/tax/calculator.ts | 372 + lib/tax/deadline-config.ts | 310 + lib/tax/deadline-generator.ts | 267 + lib/tax/expense-warnings.ts | 163 + lib/tax/light-calculator.ts | 145 + lib/tax/schablonavdrag.ts | 199 + lib/tax/swedish-holidays.ts | 235 + lib/tiktok/api.ts | 164 + lib/tiktok/encryption.ts | 105 + lib/tiktok/metrics.ts | 318 + lib/tiktok/oauth.ts | 251 + lib/tiktok/rate-limiter.ts | 107 + lib/tiktok/sync.ts | 394 + lib/tiktok/types.ts | 101 + lib/transactions/category-suggestions.ts | 152 + lib/utils.ts | 36 + middleware.ts | 20 + next.config.ts | 7 + package-lock.json | 11480 ++++++++++++++++ package.json | 66 + postcss.config.mjs | 7 + public/file.svg | 1 + public/globe.svg | 1 + public/manifest.json | 62 + public/next.svg | 1 + public/sw-register.js | 31 + public/sw.js | 93 + public/vercel.svg | 1 + public/window.svg | 1 + .../20250129_add_authorization_id.sql | 14 + .../20250129_add_potential_invoice_id.sql | 13 + .../20250129_add_schablonavdrag.sql | 60 + .../20250130_create_gifts_table.sql | 69 + .../20250130_create_sie_imports.sql | 122 + .../migrations/20250201_create_campaigns.sql | 548 + .../migrations/20250201_create_deadlines.sql | 76 + .../20250202_create_tiktok_integration.sql | 315 + .../migrations/20250203_create_briefings.sql | 83 + .../migrations/20250204_create_receipts.sql | 179 + .../20250205_create_invoice_reminders.sql | 75 + .../20250206_calendar_enhancements.sql | 229 + tsconfig.json | 34 + types/chat.ts | 55 + types/index.ts | 2518 ++++ vercel.json | 28 + 391 files changed, 96224 insertions(+) create mode 100644 .claude/skills/langchain/SKILL.md create mode 100644 .claude/skills/langchain/references/agents.md create mode 100644 .claude/skills/langchain/references/integration.md create mode 100644 .claude/skills/langchain/references/rag.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/(auth)/auth/callback/route.ts create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(dashboard)/analytics/page.tsx create mode 100644 app/(dashboard)/bookkeeping/page.tsx create mode 100644 app/(dashboard)/calendar/page.tsx create mode 100644 app/(dashboard)/campaigns/[id]/page.tsx create mode 100644 app/(dashboard)/campaigns/import/page.tsx create mode 100644 app/(dashboard)/campaigns/new/page.tsx create mode 100644 app/(dashboard)/campaigns/page.tsx create mode 100644 app/(dashboard)/customers/[id]/page.tsx create mode 100644 app/(dashboard)/customers/page.tsx create mode 100644 app/(dashboard)/deductions/page.tsx create mode 100644 app/(dashboard)/gifts/page.tsx create mode 100644 app/(dashboard)/help/page.tsx create mode 100644 app/(dashboard)/import/page.tsx create mode 100644 app/(dashboard)/invoices/[id]/credit/page.tsx create mode 100644 app/(dashboard)/invoices/[id]/page.tsx create mode 100644 app/(dashboard)/invoices/new/page.tsx create mode 100644 app/(dashboard)/invoices/page.tsx create mode 100644 app/(dashboard)/layout.tsx create mode 100644 app/(dashboard)/page.tsx create mode 100644 app/(dashboard)/receipts/page.tsx create mode 100644 app/(dashboard)/receipts/scan/page.tsx create mode 100644 app/(dashboard)/reports/page.tsx create mode 100644 app/(dashboard)/settings/page.tsx create mode 100644 app/(dashboard)/shadow-ledger/new/page.tsx create mode 100644 app/(dashboard)/shadow-ledger/page.tsx create mode 100644 app/(dashboard)/transactions/page.tsx create mode 100644 app/(onboarding)/onboarding/page.tsx create mode 100644 app/(public)/invoice-action/[token]/page.tsx create mode 100644 app/api/banking/callback/route.ts create mode 100644 app/api/banking/connect/route.ts create mode 100644 app/api/banking/sync/cron/route.ts create mode 100644 app/api/banking/sync/route.ts create mode 100644 app/api/bookkeeping/accounts/[number]/route.ts create mode 100644 app/api/bookkeeping/accounts/route.ts create mode 100644 app/api/bookkeeping/fiscal-periods/route.ts create mode 100644 app/api/bookkeeping/journal-entries/[id]/reverse/route.ts create mode 100644 app/api/bookkeeping/journal-entries/[id]/route.ts create mode 100644 app/api/bookkeeping/journal-entries/route.ts create mode 100644 app/api/bookkeeping/mapping-rules/evaluate/route.ts create mode 100644 app/api/bookkeeping/mapping-rules/route.ts create mode 100644 app/api/briefings/[id]/download/route.ts create mode 100644 app/api/briefings/[id]/route.ts create mode 100644 app/api/briefings/summarize/route.ts create mode 100644 app/api/calendar/feed/[token]/route.ts create mode 100644 app/api/calendar/feed/route.ts create mode 100644 app/api/campaigns/[id]/briefings/route.ts create mode 100644 app/api/campaigns/[id]/briefings/upload/route.ts create mode 100644 app/api/campaigns/[id]/contracts/route.ts create mode 100644 app/api/campaigns/[id]/deliverables/route.ts create mode 100644 app/api/campaigns/[id]/exclusivities/route.ts create mode 100644 app/api/campaigns/[id]/route.ts create mode 100644 app/api/campaigns/from-contract/route.ts create mode 100644 app/api/campaigns/route.ts create mode 100644 app/api/campaigns/workload/route.ts create mode 100644 app/api/chat/route.ts create mode 100644 app/api/chat/sessions/[id]/route.ts create mode 100644 app/api/chat/sessions/route.ts create mode 100644 app/api/chat/stream/route.ts create mode 100644 app/api/contracts/[id]/download/route.ts create mode 100644 app/api/contracts/[id]/extract/route.ts create mode 100644 app/api/contracts/[id]/route.ts create mode 100644 app/api/customers/[id]/route.ts create mode 100644 app/api/customers/route.ts create mode 100644 app/api/deadlines/[id]/complete/route.ts create mode 100644 app/api/deadlines/[id]/route.ts create mode 100644 app/api/deadlines/[id]/status/route.ts create mode 100644 app/api/deadlines/route.ts create mode 100644 app/api/deadlines/status/cron/route.ts create mode 100644 app/api/deliverables/[id]/route.ts create mode 100644 app/api/deliverables/[id]/status/route.ts create mode 100644 app/api/exclusivities/[id]/route.ts create mode 100644 app/api/exclusivities/conflicts/route.ts create mode 100644 app/api/gifts/[id]/route.ts create mode 100644 app/api/gifts/estimate/route.ts create mode 100644 app/api/gifts/route.ts create mode 100644 app/api/gifts/summary/route.ts create mode 100644 app/api/import/sie/[id]/route.ts create mode 100644 app/api/import/sie/create-accounts/route.ts create mode 100644 app/api/import/sie/execute/route.ts create mode 100644 app/api/import/sie/mappings/route.ts create mode 100644 app/api/import/sie/parse/route.ts create mode 100644 app/api/import/sie/route.ts create mode 100644 app/api/invoices/[id]/pdf/route.ts create mode 100644 app/api/invoices/[id]/send/route.ts create mode 100644 app/api/invoices/reminders/action/route.ts create mode 100644 app/api/invoices/reminders/cron/route.ts create mode 100644 app/api/invoices/route.ts create mode 100644 app/api/push/cron/route.ts create mode 100644 app/api/push/subscribe/route.ts create mode 100644 app/api/receipts/[id]/confirm/route.ts create mode 100644 app/api/receipts/[id]/match/route.ts create mode 100644 app/api/receipts/[id]/route.ts create mode 100644 app/api/receipts/product/route.ts create mode 100644 app/api/receipts/queue/route.ts create mode 100644 app/api/receipts/route.ts create mode 100644 app/api/receipts/upload/route.ts create mode 100644 app/api/reports/balance-sheet/route.ts create mode 100644 app/api/reports/income-statement/route.ts create mode 100644 app/api/reports/ne-declaration/route.ts create mode 100644 app/api/reports/sie-export/route.ts create mode 100644 app/api/reports/trial-balance/route.ts create mode 100644 app/api/reports/vat-declaration/route.ts create mode 100644 app/api/settings/route.ts create mode 100644 app/api/shadow-ledger/[id]/route.ts create mode 100644 app/api/shadow-ledger/route.ts create mode 100644 app/api/shadow-ledger/summary/route.ts create mode 100644 app/api/tax-deadlines/cron/route.ts create mode 100644 app/api/tax-deadlines/generate/route.ts create mode 100644 app/api/tiktok/accounts/route.ts create mode 100644 app/api/tiktok/callback/route.ts create mode 100644 app/api/tiktok/connect/route.ts create mode 100644 app/api/tiktok/cron/route.ts create mode 100644 app/api/tiktok/disconnect/route.ts create mode 100644 app/api/tiktok/stats/route.ts create mode 100644 app/api/tiktok/sync/route.ts create mode 100644 app/api/tiktok/videos/[id]/link/route.ts create mode 100644 app/api/tiktok/videos/route.ts create mode 100644 app/api/transactions/[id]/categorize/route.ts create mode 100644 app/api/transactions/[id]/match-invoice/route.ts create mode 100644 app/api/transactions/batch-match-invoices/route.ts create mode 100644 app/api/transactions/suggest-categories/route.ts create mode 100644 app/api/vat/validate/route.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components/banking/BankConnectionStatus.tsx create mode 100644 components/banking/BankSelector.tsx create mode 100644 components/banking/index.ts create mode 100644 components/benefits/GiftForm.tsx create mode 100644 components/benefits/GiftList.tsx create mode 100644 components/benefits/GiftSummaryCard.tsx create mode 100644 components/bookkeeping/ChartOfAccounts.tsx create mode 100644 components/bookkeeping/JournalEntryForm.tsx create mode 100644 components/bookkeeping/JournalEntryList.tsx create mode 100644 components/calendar/CalendarDayCell.tsx create mode 100644 components/calendar/CalendarDayView.tsx create mode 100644 components/calendar/CalendarGrid.tsx create mode 100644 components/calendar/CalendarHeader.tsx create mode 100644 components/calendar/CalendarWeekView.tsx create mode 100644 components/calendar/DayDetailModal.tsx create mode 100644 components/calendar/DeadlineCard.tsx create mode 100644 components/calendar/DeadlineFilters.tsx create mode 100644 components/calendar/DeadlineForm.tsx create mode 100644 components/calendar/DeadlineList.tsx create mode 100644 components/calendar/PaymentCalendar.tsx create mode 100644 components/calendar/PaymentSummaryCard.tsx create mode 100644 components/calendar/TaxTodoWidget.tsx create mode 100644 components/calendar/UpcomingDeadlinesWidget.tsx create mode 100644 components/calendar/ViewModeSelector.tsx create mode 100644 components/calendar/index.ts create mode 100644 components/campaigns/BriefingForm.tsx create mode 100644 components/campaigns/BriefingList.tsx create mode 100644 components/campaigns/CampaignCard.tsx create mode 100644 components/campaigns/CampaignDetail.tsx create mode 100644 components/campaigns/CampaignForm.tsx create mode 100644 components/campaigns/CampaignInvoiceSummary.tsx create mode 100644 components/campaigns/CampaignList.tsx create mode 100644 components/campaigns/CampaignStatusBadge.tsx create mode 100644 components/campaigns/CampaignsWidget.tsx create mode 100644 components/campaigns/ContractList.tsx create mode 100644 components/campaigns/ContractUpload.tsx create mode 100644 components/campaigns/DeliverableCard.tsx create mode 100644 components/campaigns/DeliverableForm.tsx create mode 100644 components/campaigns/DeliverableList.tsx create mode 100644 components/campaigns/ExclusivityForm.tsx create mode 100644 components/campaigns/ExclusivityList.tsx create mode 100644 components/campaigns/index.ts create mode 100644 components/chat/ChatInput.tsx create mode 100644 components/chat/ChatMessage.tsx create mode 100644 components/chat/ChatPanel.tsx create mode 100644 components/chat/ChatWidget.tsx create mode 100644 components/chat/index.ts create mode 100644 components/chat/useChatStream.ts create mode 100644 components/contracts/ConfidenceIndicator.tsx create mode 100644 components/contracts/ContractImportWizard.tsx create mode 100644 components/contracts/ExtractionStatusBadge.tsx create mode 100644 components/customers/CustomerForm.tsx create mode 100644 components/dashboard/DashboardContent.tsx create mode 100644 components/dashboard/DashboardNav.tsx create mode 100644 components/dashboard/FSkattWarningCard.tsx create mode 100644 components/dashboard/GiftTaxDebtCard.tsx create mode 100644 components/dashboard/IncomeChart.tsx create mode 100644 components/dashboard/LightDashboardContent.tsx create mode 100644 components/dashboard/RecentPayoutsCard.tsx create mode 100644 components/dashboard/SGIShieldWidget.tsx create mode 100644 components/dashboard/SafeToSpendGauge.tsx create mode 100644 components/import/AccountMappingStep.tsx create mode 100644 components/import/ImportResultStep.tsx create mode 100644 components/import/ImportReviewStep.tsx create mode 100644 components/import/SIEPreviewStep.tsx create mode 100644 components/import/SIEUploadStep.tsx create mode 100644 components/onboarding/DashboardTour.tsx create mode 100644 components/onboarding/NewUserChecklist.tsx create mode 100644 components/onboarding/Step1EntityType.tsx create mode 100644 components/onboarding/Step2CompanyDetails.tsx create mode 100644 components/onboarding/Step2LightPersonalInfo.tsx create mode 100644 components/onboarding/Step3LightTaxProfile.tsx create mode 100644 components/onboarding/Step3TaxRegistration.tsx create mode 100644 components/onboarding/Step4PreliminaryTax.tsx create mode 100644 components/onboarding/Step5BankDetails.tsx create mode 100644 components/onboarding/Step6ConnectBank.tsx create mode 100644 components/push/PushPrompt.tsx create mode 100644 components/receipts/ProductCapture.tsx create mode 100644 components/receipts/ReceiptCamera.tsx create mode 100644 components/receipts/ReceiptDashboard.tsx create mode 100644 components/receipts/ReceiptLineItemRow.tsx create mode 100644 components/receipts/ReceiptReviewView.tsx create mode 100644 components/receipts/TransactionMatcher.tsx create mode 100644 components/receipts/index.ts create mode 100644 components/settings/CalendarFeedSettings.tsx create mode 100644 components/settings/NotificationSettings.tsx create mode 100644 components/settings/SchablonavdragSettings.tsx create mode 100644 components/shadow-ledger/PayoutWaterfall.tsx create mode 100644 components/shadow-ledger/ShadowLedgerForm.tsx create mode 100644 components/shadow-ledger/ShadowLedgerList.tsx create mode 100644 components/tiktok/TikTokAccountCard.tsx create mode 100644 components/tiktok/TikTokConnectButton.tsx create mode 100644 components/tiktok/TikTokGrowthChart.tsx create mode 100644 components/tiktok/TikTokROITable.tsx create mode 100644 components/tiktok/TikTokStatsWidget.tsx create mode 100644 components/tiktok/TikTokVideoCard.tsx create mode 100644 components/tiktok/TikTokVideoList.tsx create mode 100644 components/tiktok/VideoLinkModal.tsx create mode 100644 components/tiktok/index.ts create mode 100644 components/transactions/MileageEntry.tsx create mode 100644 components/transactions/SwipeCategorizationView.tsx create mode 100644 components/transactions/TransactionForm.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/empty-state.tsx create mode 100644 components/ui/info-tooltip.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/page-header.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/segmented-progress.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/success-animation.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/use-toast.tsx create mode 100644 dev_docs/01-PRD.md create mode 100644 dev_docs/02-ARCHITECTURE.md create mode 100644 dev_docs/03-DATABASE-SCHEMA.md create mode 100644 dev_docs/04-API-SPECIFICATION.md create mode 100644 dev_docs/05-UI-SPECIFICATION.md create mode 100644 dev_docs/06-IMPLEMENTATION-GUIDE.md create mode 100644 dev_docs/07-FUTURE-FEATURES.md create mode 100644 dev_docs/08-BAS-ACCOUNTING-GUIDE.md create mode 100644 dev_docs/ALICE_DATA_02_10.json create mode 100644 dev_docs/ENABLE_BANKING_DOCS/API_EB.md create mode 100644 dev_docs/ENABLE_BANKING_DOCS/BUSINESS_BANKING_SETUP.md create mode 100644 dev_docs/ENABLE_BANKING_DOCS/EB_UI_WIDGETS.md create mode 100644 dev_docs/ENABLE_BANKING_DOCS/QUICKSTART_EB.md create mode 100644 dev_docs/ENABLE_BANKING_DOCS/QUICKSTART_EB_GK.md create mode 100644 dev_docs/FEEDBACK_02_09.md create mode 100644 dev_docs/FULL_BAS.md create mode 100644 dev_docs/K1_BAS.md create mode 100644 dev_docs/NYA_FEATURES.md create mode 100644 dev_docs/README.md create mode 100644 dev_docs/archive/alice_bank_data.json create mode 100644 dev_docs/influencer_ai_info/00-ai-snabbreferens.md create mode 100644 dev_docs/influencer_ai_info/01-karnregler.md create mode 100644 dev_docs/influencer_ai_info/02-scenariobibliotek.md create mode 100644 dev_docs/influencer_ai_info/03-plattformskatalog.md create mode 100644 dev_docs/influencer_ai_info/04-berakningsexempel.md create mode 100644 dev_docs/influencer_ai_info/05-beslutstrад.md create mode 100644 dev_docs/influencer_ai_info/06-felsokningsguide.md create mode 100644 dev_docs/influencer_ai_info/07-kalender-deadlines.md create mode 100644 dev_docs/influencer_ai_info/08-varningsflaggor.md create mode 100644 eslint.config.mjs create mode 100644 lib/ai/chatbot/chain.ts create mode 100644 lib/ai/chatbot/config.ts create mode 100644 lib/ai/chatbot/embeddings.ts create mode 100644 lib/ai/chatbot/prompts.ts create mode 100644 lib/ai/chatbot/retriever.ts create mode 100644 lib/ai/ingestion/ingest.ts create mode 100644 lib/banking/enable-banking.ts create mode 100644 lib/banking/jwt.ts create mode 100644 lib/banking/sync-transactions.ts create mode 100644 lib/benefits/gift-booking.ts create mode 100644 lib/benefits/gift-classifier.ts create mode 100644 lib/bookkeeping/category-mapping.ts create mode 100644 lib/bookkeeping/engine.ts create mode 100644 lib/bookkeeping/invoice-entries.ts create mode 100644 lib/bookkeeping/mapping-engine.ts create mode 100644 lib/bookkeeping/transaction-entries.ts create mode 100644 lib/bookkeeping/vat-entries.ts create mode 100644 lib/calendar/ics-generator.ts create mode 100644 lib/calendar/utils.ts create mode 100644 lib/campaigns/deadline-generator.ts create mode 100644 lib/campaigns/exclusivity-checker.ts create mode 100644 lib/campaigns/index.ts create mode 100644 lib/campaigns/payment-tracker.ts create mode 100644 lib/campaigns/workload-analyzer.ts create mode 100644 lib/contracts/contract-analyzer.ts create mode 100644 lib/contracts/pdf-extractor.ts create mode 100644 lib/currency/riksbanken.ts create mode 100644 lib/customers/customer-matcher.ts create mode 100644 lib/deadlines/status-engine.ts create mode 100644 lib/email/invoice-templates.ts create mode 100644 lib/email/reminder-templates.ts create mode 100644 lib/email/resend.ts create mode 100644 lib/import/account-mapper.ts create mode 100644 lib/import/sie-import.ts create mode 100644 lib/import/sie-parser.ts create mode 100644 lib/import/types.ts create mode 100644 lib/invoice/invoice-matching.ts create mode 100644 lib/invoice/pdf-template.tsx create mode 100644 lib/invoice/vat-rules.ts create mode 100644 lib/invoices/reminder-processor.ts create mode 100644 lib/light/dashboard-data.ts create mode 100644 lib/push/notification-scheduler.ts create mode 100644 lib/push/web-push.ts create mode 100644 lib/receipts/receipt-analyzer.ts create mode 100644 lib/receipts/receipt-categorizer.ts create mode 100644 lib/receipts/receipt-matcher.ts create mode 100644 lib/receipts/receipt-utils.ts create mode 100644 lib/reports/balance-sheet.ts create mode 100644 lib/reports/income-statement.ts create mode 100644 lib/reports/ne-declaration.ts create mode 100644 lib/reports/sie-export.ts create mode 100644 lib/reports/sru-generator.ts create mode 100644 lib/reports/trial-balance.ts create mode 100644 lib/reports/vat-declaration.ts create mode 100644 lib/supabase/client.ts create mode 100644 lib/supabase/middleware.ts create mode 100644 lib/supabase/server.ts create mode 100644 lib/tax/calculator.ts create mode 100644 lib/tax/deadline-config.ts create mode 100644 lib/tax/deadline-generator.ts create mode 100644 lib/tax/expense-warnings.ts create mode 100644 lib/tax/light-calculator.ts create mode 100644 lib/tax/schablonavdrag.ts create mode 100644 lib/tax/swedish-holidays.ts create mode 100644 lib/tiktok/api.ts create mode 100644 lib/tiktok/encryption.ts create mode 100644 lib/tiktok/metrics.ts create mode 100644 lib/tiktok/oauth.ts create mode 100644 lib/tiktok/rate-limiter.ts create mode 100644 lib/tiktok/sync.ts create mode 100644 lib/tiktok/types.ts create mode 100644 lib/transactions/category-suggestions.ts create mode 100644 lib/utils.ts create mode 100644 middleware.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/file.svg create mode 100644 public/globe.svg create mode 100644 public/manifest.json create mode 100644 public/next.svg create mode 100644 public/sw-register.js create mode 100644 public/sw.js create mode 100644 public/vercel.svg create mode 100644 public/window.svg create mode 100644 supabase/migrations/20250129_add_authorization_id.sql create mode 100644 supabase/migrations/20250129_add_potential_invoice_id.sql create mode 100644 supabase/migrations/20250129_add_schablonavdrag.sql create mode 100644 supabase/migrations/20250130_create_gifts_table.sql create mode 100644 supabase/migrations/20250130_create_sie_imports.sql create mode 100644 supabase/migrations/20250201_create_campaigns.sql create mode 100644 supabase/migrations/20250201_create_deadlines.sql create mode 100644 supabase/migrations/20250202_create_tiktok_integration.sql create mode 100644 supabase/migrations/20250203_create_briefings.sql create mode 100644 supabase/migrations/20250204_create_receipts.sql create mode 100644 supabase/migrations/20250205_create_invoice_reminders.sql create mode 100644 supabase/migrations/20250206_calendar_enhancements.sql create mode 100644 tsconfig.json create mode 100644 types/chat.ts create mode 100644 types/index.ts create mode 100644 vercel.json diff --git a/.claude/skills/langchain/SKILL.md b/.claude/skills/langchain/SKILL.md new file mode 100644 index 00000000..cf667923 --- /dev/null +++ b/.claude/skills/langchain/SKILL.md @@ -0,0 +1,480 @@ +--- +name: langchain +description: Framework for building LLM-powered applications with agents, chains, and RAG. Supports multiple providers (OpenAI, Anthropic, Google), 500+ integrations, ReAct agents, tool calling, memory management, and vector store retrieval. Use for building chatbots, question-answering systems, autonomous agents, or RAG applications. Best for rapid prototyping and production deployments. +version: 1.0.0 +author: Orchestra Research +license: MIT +tags: [Agents, LangChain, RAG, Tool Calling, ReAct, Memory Management, Vector Stores, LLM Applications, Chatbots, Production] +dependencies: [langchain, langchain-core, langchain-openai, langchain-anthropic] +--- + +# LangChain - Build LLM Applications with Agents & RAG + +The most popular framework for building LLM-powered applications. + +## When to use LangChain + +**Use LangChain when:** +- Building agents with tool calling and reasoning (ReAct pattern) +- Implementing RAG (retrieval-augmented generation) pipelines +- Need to swap LLM providers easily (OpenAI, Anthropic, Google) +- Creating chatbots with conversation memory +- Rapid prototyping of LLM applications +- Production deployments with LangSmith observability + +**Metrics**: +- **119,000+ GitHub stars** +- **272,000+ repositories** use LangChain +- **500+ integrations** (models, vector stores, tools) +- **3,800+ contributors** + +**Use alternatives instead**: +- **LlamaIndex**: RAG-focused, better for document Q&A +- **LangGraph**: Complex stateful workflows, more control +- **Haystack**: Production search pipelines +- **Semantic Kernel**: Microsoft ecosystem + +## Quick start + +### Installation + +```bash +# Core library (Python 3.10+) +pip install -U langchain + +# With OpenAI +pip install langchain-openai + +# With Anthropic +pip install langchain-anthropic + +# Common extras +pip install langchain-community # 500+ integrations +pip install langchain-chroma # Vector store +``` + +### Basic LLM usage + +```python +from langchain_anthropic import ChatAnthropic + +# Initialize model +llm = ChatAnthropic(model="claude-sonnet-4-5-20250929") + +# Simple completion +response = llm.invoke("Explain quantum computing in 2 sentences") +print(response.content) +``` + +### Create an agent (ReAct pattern) + +```python +from langchain.agents import create_agent +from langchain_anthropic import ChatAnthropic + +# Define tools +def get_weather(city: str) -> str: + """Get current weather for a city.""" + return f"It's sunny in {city}, 72°F" + +def search_web(query: str) -> str: + """Search the web for information.""" + return f"Search results for: {query}" + +# Create agent (<10 lines!) +agent = create_agent( + model=ChatAnthropic(model="claude-sonnet-4-5-20250929"), + tools=[get_weather, search_web], + system_prompt="You are a helpful assistant. Use tools when needed." +) + +# Run agent +result = agent.invoke({"messages": [{"role": "user", "content": "What's the weather in Paris?"}]}) +print(result["messages"][-1].content) +``` + +## Core concepts + +### 1. Models - LLM abstraction + +```python +from langchain_openai import ChatOpenAI +from langchain_anthropic import ChatAnthropic +from langchain_google_genai import ChatGoogleGenerativeAI + +# Swap providers easily +llm = ChatOpenAI(model="gpt-4o") +llm = ChatAnthropic(model="claude-sonnet-4-5-20250929") +llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash-exp") + +# Streaming +for chunk in llm.stream("Write a poem"): + print(chunk.content, end="", flush=True) +``` + +### 2. Chains - Sequential operations + +```python +from langchain.chains import LLMChain +from langchain.prompts import PromptTemplate + +# Define prompt template +prompt = PromptTemplate( + input_variables=["topic"], + template="Write a 3-sentence summary about {topic}" +) + +# Create chain +chain = LLMChain(llm=llm, prompt=prompt) + +# Run chain +result = chain.run(topic="machine learning") +``` + +### 3. Agents - Tool-using reasoning + +**ReAct (Reasoning + Acting) pattern:** + +```python +from langchain.agents import create_tool_calling_agent, AgentExecutor +from langchain.tools import Tool + +# Define custom tool +calculator = Tool( + name="Calculator", + func=lambda x: eval(x), + description="Useful for math calculations. Input: valid Python expression." +) + +# Create agent with tools +agent = create_tool_calling_agent( + llm=llm, + tools=[calculator, search_web], + prompt="Answer questions using available tools" +) + +# Create executor +agent_executor = AgentExecutor(agent=agent, tools=[calculator], verbose=True) + +# Run with reasoning +result = agent_executor.invoke({"input": "What is 25 * 17 + 142?"}) +``` + +### 4. Memory - Conversation history + +```python +from langchain.memory import ConversationBufferMemory +from langchain.chains import ConversationChain + +# Add memory to track conversation +memory = ConversationBufferMemory() + +conversation = ConversationChain( + llm=llm, + memory=memory, + verbose=True +) + +# Multi-turn conversation +conversation.predict(input="Hi, I'm Alice") +conversation.predict(input="What's my name?") # Remembers "Alice" +``` + +## RAG (Retrieval-Augmented Generation) + +### Basic RAG pipeline + +```python +from langchain_community.document_loaders import WebBaseLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_openai import OpenAIEmbeddings +from langchain_chroma import Chroma +from langchain.chains import RetrievalQA + +# 1. Load documents +loader = WebBaseLoader("https://docs.python.org/3/tutorial/") +docs = loader.load() + +# 2. Split into chunks +text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200 +) +splits = text_splitter.split_documents(docs) + +# 3. Create embeddings and vector store +vectorstore = Chroma.from_documents( + documents=splits, + embedding=OpenAIEmbeddings() +) + +# 4. Create retriever +retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) + +# 5. Create QA chain +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + return_source_documents=True +) + +# 6. Query +result = qa_chain({"query": "What are Python decorators?"}) +print(result["result"]) +print(f"Sources: {result['source_documents']}") +``` + +### Conversational RAG with memory + +```python +from langchain.chains import ConversationalRetrievalChain + +# RAG with conversation memory +qa = ConversationalRetrievalChain.from_llm( + llm=llm, + retriever=retriever, + memory=ConversationBufferMemory( + memory_key="chat_history", + return_messages=True + ) +) + +# Multi-turn RAG +qa({"question": "What is Python used for?"}) +qa({"question": "Can you elaborate on web development?"}) # Remembers context +``` + +## Advanced agent patterns + +### Structured output + +```python +from langchain_core.pydantic_v1 import BaseModel, Field + +# Define schema +class WeatherReport(BaseModel): + city: str = Field(description="City name") + temperature: float = Field(description="Temperature in Fahrenheit") + condition: str = Field(description="Weather condition") + +# Get structured response +structured_llm = llm.with_structured_output(WeatherReport) +result = structured_llm.invoke("What's the weather in SF? It's 65F and sunny") +print(result.city, result.temperature, result.condition) +``` + +### Parallel tool execution + +```python +from langchain.agents import create_tool_calling_agent + +# Agent automatically parallelizes independent tool calls +agent = create_tool_calling_agent( + llm=llm, + tools=[get_weather, search_web, calculator] +) + +# This will call get_weather("Paris") and get_weather("London") in parallel +result = agent.invoke({ + "messages": [{"role": "user", "content": "Compare weather in Paris and London"}] +}) +``` + +### Streaming agent execution + +```python +# Stream agent steps +for step in agent_executor.stream({"input": "Research AI trends"}): + if "actions" in step: + print(f"Tool: {step['actions'][0].tool}") + if "output" in step: + print(f"Output: {step['output']}") +``` + +## Common patterns + +### Multi-document QA + +```python +from langchain.chains.qa_with_sources import load_qa_with_sources_chain + +# Load multiple documents +docs = [ + loader.load("https://docs.python.org"), + loader.load("https://docs.numpy.org") +] + +# QA with source citations +chain = load_qa_with_sources_chain(llm, chain_type="stuff") +result = chain({"input_documents": docs, "question": "How to use numpy arrays?"}) +print(result["output_text"]) # Includes source citations +``` + +### Custom tools with error handling + +```python +from langchain.tools import tool + +@tool +def risky_operation(query: str) -> str: + """Perform a risky operation that might fail.""" + try: + # Your operation here + result = perform_operation(query) + return f"Success: {result}" + except Exception as e: + return f"Error: {str(e)}" + +# Agent handles errors gracefully +agent = create_agent(model=llm, tools=[risky_operation]) +``` + +### LangSmith observability + +```python +import os + +# Enable tracing +os.environ["LANGCHAIN_TRACING_V2"] = "true" +os.environ["LANGCHAIN_API_KEY"] = "your-api-key" +os.environ["LANGCHAIN_PROJECT"] = "my-project" + +# All chains/agents automatically traced +agent = create_agent(model=llm, tools=[calculator]) +result = agent.invoke({"input": "Calculate 123 * 456"}) + +# View traces at smith.langchain.com +``` + +## Vector stores + +### Chroma (local) + +```python +from langchain_chroma import Chroma + +vectorstore = Chroma.from_documents( + documents=docs, + embedding=OpenAIEmbeddings(), + persist_directory="./chroma_db" +) +``` + +### Pinecone (cloud) + +```python +from langchain_pinecone import PineconeVectorStore + +vectorstore = PineconeVectorStore.from_documents( + documents=docs, + embedding=OpenAIEmbeddings(), + index_name="my-index" +) +``` + +### FAISS (similarity search) + +```python +from langchain_community.vectorstores import FAISS + +vectorstore = FAISS.from_documents(docs, OpenAIEmbeddings()) +vectorstore.save_local("faiss_index") + +# Load later +vectorstore = FAISS.load_local("faiss_index", OpenAIEmbeddings()) +``` + +## Document loaders + +```python +# Web pages +from langchain_community.document_loaders import WebBaseLoader +loader = WebBaseLoader("https://example.com") + +# PDFs +from langchain_community.document_loaders import PyPDFLoader +loader = PyPDFLoader("paper.pdf") + +# GitHub +from langchain_community.document_loaders import GithubFileLoader +loader = GithubFileLoader(repo="user/repo", file_filter=lambda x: x.endswith(".py")) + +# CSV +from langchain_community.document_loaders import CSVLoader +loader = CSVLoader("data.csv") +``` + +## Text splitters + +```python +# Recursive (recommended for general text) +from langchain.text_splitter import RecursiveCharacterTextSplitter +splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200, + separators=["\n\n", "\n", " ", ""] +) + +# Code-aware +from langchain.text_splitter import PythonCodeTextSplitter +splitter = PythonCodeTextSplitter(chunk_size=500) + +# Semantic (by meaning) +from langchain_experimental.text_splitter import SemanticChunker +splitter = SemanticChunker(OpenAIEmbeddings()) +``` + +## Best practices + +1. **Start simple** - Use `create_agent()` for most cases +2. **Enable streaming** - Better UX for long responses +3. **Add error handling** - Tools can fail, handle gracefully +4. **Use LangSmith** - Essential for debugging agents +5. **Optimize chunk size** - 500-1000 chars for RAG +6. **Version prompts** - Track changes in production +7. **Cache embeddings** - Expensive, cache when possible +8. **Monitor costs** - Track token usage with LangSmith + +## Performance benchmarks + +| Operation | Latency | Notes | +|-----------|---------|-------| +| Simple LLM call | ~1-2s | Depends on provider | +| Agent with 1 tool | ~3-5s | ReAct reasoning overhead | +| RAG retrieval | ~0.5-1s | Vector search + LLM | +| Embedding 1000 docs | ~10-30s | Depends on model | + +## LangChain vs LangGraph + +| Feature | LangChain | LangGraph | +|---------|-----------|-----------| +| **Best for** | Quick agents, RAG | Complex workflows | +| **Abstraction level** | High | Low | +| **Code to start** | <10 lines | ~30 lines | +| **Control** | Simple | Full control | +| **Stateful workflows** | Limited | Native | +| **Cyclic graphs** | No | Yes | +| **Human-in-loop** | Basic | Advanced | + +**Use LangGraph when:** +- Need stateful workflows with cycles +- Require fine-grained control +- Building multi-agent systems +- Production apps with complex logic + +## References + +- **[Agents Guide](references/agents.md)** - ReAct, tool calling, streaming +- **[RAG Guide](references/rag.md)** - Document loaders, retrievers, QA chains +- **[Integration Guide](references/integration.md)** - Vector stores, LangSmith, deployment + +## Resources + +- **GitHub**: https://github.com/langchain-ai/langchain ⭐ 119,000+ +- **Docs**: https://docs.langchain.com +- **API Reference**: https://reference.langchain.com/python +- **LangSmith**: https://smith.langchain.com (observability) +- **Version**: 0.3+ (stable) +- **License**: MIT + + diff --git a/.claude/skills/langchain/references/agents.md b/.claude/skills/langchain/references/agents.md new file mode 100644 index 00000000..fe8e6fee --- /dev/null +++ b/.claude/skills/langchain/references/agents.md @@ -0,0 +1,499 @@ +# LangChain Agents Guide + +Complete guide to building agents with ReAct, tool calling, and streaming. + +## What are agents? + +Agents combine language models with tools to solve complex tasks through reasoning and action: + +1. **Reasoning**: LLM decides what to do +2. **Acting**: Execute tools based on reasoning +3. **Observation**: Receive tool results +4. **Loop**: Repeat until task complete + +This is the **ReAct pattern** (Reasoning + Acting). + +## Basic agent creation + +```python +from langchain.agents import create_agent +from langchain_anthropic import ChatAnthropic + +# Define tools +def calculator(expression: str) -> str: + """Evaluate a math expression.""" + return str(eval(expression)) + +def search(query: str) -> str: + """Search for information.""" + return f"Results for: {query}" + +# Create agent +agent = create_agent( + model=ChatAnthropic(model="claude-sonnet-4-5-20250929"), + tools=[calculator, search], + system_prompt="You are a helpful assistant. Use tools when needed." +) + +# Run agent +result = agent.invoke({ + "messages": [{"role": "user", "content": "What is 25 * 17?"}] +}) +print(result["messages"][-1].content) +``` + +## Agent components + +### 1. Model - The reasoning engine + +```python +from langchain_openai import ChatOpenAI +from langchain_anthropic import ChatAnthropic + +# OpenAI +model = ChatOpenAI(model="gpt-4o", temperature=0) + +# Anthropic (better for complex reasoning) +model = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0) + +# Dynamic model selection +def select_model(task_complexity: str): + if task_complexity == "high": + return ChatAnthropic(model="claude-sonnet-4-5-20250929") + else: + return ChatOpenAI(model="gpt-4o-mini") +``` + +### 2. Tools - Actions the agent can take + +```python +from langchain.tools import tool + +# Simple function tool +@tool +def get_current_time() -> str: + """Get the current time.""" + from datetime import datetime + return datetime.now().strftime("%H:%M:%S") + +# Tool with parameters +@tool +def fetch_weather(city: str, units: str = "fahrenheit") -> str: + """Fetch weather for a city. + + Args: + city: City name + units: Temperature units (fahrenheit or celsius) + """ + # Your weather API call here + return f"Weather in {city}: 72°{units[0].upper()}" + +# Tool with error handling +@tool +def risky_api_call(endpoint: str) -> str: + """Call an external API that might fail.""" + try: + response = requests.get(endpoint, timeout=5) + return response.text + except Exception as e: + return f"Error calling API: {str(e)}" +``` + +### 3. System prompt - Agent behavior + +```python +# General assistant +system_prompt = "You are a helpful assistant. Use tools when needed." + +# Domain expert +system_prompt = """You are a financial analyst assistant. +- Use the calculator for precise calculations +- Search for recent financial data +- Provide data-driven recommendations +- Always cite your sources""" + +# Constrained agent +system_prompt = """You are a customer support agent. +- Only use search_kb tool to find answers +- If answer not found, escalate to human +- Be concise and professional +- Never make up information""" +``` + +## Agent types + +### 1. Tool-calling agent (recommended) + +Uses native function calling for best performance: + +```python +from langchain.agents import create_tool_calling_agent, AgentExecutor +from langchain.prompts import ChatPromptTemplate + +# Create prompt +prompt = ChatPromptTemplate.from_messages([ + ("system", "You are a helpful assistant"), + ("human", "{input}"), + ("placeholder", "{agent_scratchpad}"), +]) + +# Create agent +agent = create_tool_calling_agent( + llm=model, + tools=[calculator, search], + prompt=prompt +) + +# Wrap in executor +agent_executor = AgentExecutor( + agent=agent, + tools=[calculator, search], + verbose=True, + max_iterations=5, + handle_parsing_errors=True +) + +# Run +result = agent_executor.invoke({"input": "What is the weather in Paris?"}) +``` + +### 2. ReAct agent (reasoning trace) + +Shows step-by-step reasoning: + +```python +from langchain.agents import create_react_agent + +# ReAct prompt shows thought process +react_prompt = """Answer the following questions as best you can. You have access to the following tools: + +{tools} + +Use the following format: + +Question: the input question you must answer +Thought: you should always think about what to do +Action: the action to take, should be one of [{tool_names}] +Action Input: the input to the action +Observation: the result of the action +... (this Thought/Action/Action Input/Observation can repeat N times) +Thought: I now know the final answer +Final Answer: the final answer to the original input question + +Begin! + +Question: {input} +Thought: {agent_scratchpad}""" + +agent = create_react_agent( + llm=model, + tools=[calculator, search], + prompt=ChatPromptTemplate.from_template(react_prompt) +) + +# Run with visible reasoning +result = agent_executor.invoke({"input": "What is 25 * 17 + 142?"}) +``` + +### 3. Conversational agent (with memory) + +Remembers conversation history: + +```python +from langchain.agents import create_conversational_retrieval_agent +from langchain.memory import ConversationBufferMemory + +# Add memory +memory = ConversationBufferMemory( + memory_key="chat_history", + return_messages=True +) + +# Conversational agent +agent_executor = AgentExecutor( + agent=agent, + tools=[calculator, search], + memory=memory, + verbose=True +) + +# Multi-turn conversation +agent_executor.invoke({"input": "My name is Alice"}) +agent_executor.invoke({"input": "What's my name?"}) # Remembers "Alice" +agent_executor.invoke({"input": "What is 25 * 17?"}) +``` + +## Tool execution patterns + +### Parallel tool execution + +```python +# Agent automatically parallelizes independent calls +agent = create_tool_calling_agent(llm=model, tools=[get_weather, search]) + +# This calls get_weather("Paris") and get_weather("London") in parallel +result = agent_executor.invoke({ + "input": "Compare weather in Paris and London" +}) +``` + +### Sequential tool chaining + +```python +# Agent chains tools automatically +@tool +def search_company(name: str) -> str: + """Search for company information.""" + return f"Company ID: 12345, Industry: Tech" + +@tool +def get_stock_price(company_id: str) -> str: + """Get stock price for a company.""" + return f"${150.00}" + +# Agent will: search_company → get_stock_price +result = agent_executor.invoke({ + "input": "What is Apple's current stock price?" +}) +``` + +### Conditional tool usage + +```python +# Agent decides when to use tools +@tool +def expensive_tool(query: str) -> str: + """Use only when necessary - costs $0.10 per call.""" + return perform_expensive_operation(query) + +# Agent uses tool only if needed +result = agent_executor.invoke({ + "input": "What is 2+2?" # Won't use expensive_tool +}) +``` + +## Streaming + +### Stream agent steps + +```python +# Stream intermediate steps +for step in agent_executor.stream({"input": "Research quantum computing"}): + if "actions" in step: + action = step["actions"][0] + print(f"Tool: {action.tool}, Input: {action.tool_input}") + if "steps" in step: + print(f"Observation: {step['steps'][0].observation}") + if "output" in step: + print(f"Final: {step['output']}") +``` + +### Stream LLM tokens + +```python +from langchain.callbacks import StreamingStdOutCallbackHandler + +# Stream model responses +agent_executor = AgentExecutor( + agent=agent, + tools=[calculator], + callbacks=[StreamingStdOutCallbackHandler()], + verbose=True +) + +result = agent_executor.invoke({"input": "Explain quantum computing"}) +``` + +## Error handling + +### Tool error handling + +```python +@tool +def fallible_tool(query: str) -> str: + """A tool that might fail.""" + try: + result = risky_operation(query) + return f"Success: {result}" + except Exception as e: + return f"Error: {str(e)}. Please try a different approach." + +# Agent adapts to errors +agent_executor = AgentExecutor( + agent=agent, + tools=[fallible_tool], + handle_parsing_errors=True, # Handle malformed tool calls + max_iterations=5 +) +``` + +### Timeout handling + +```python +from langchain.callbacks import TimeoutCallback + +# Set timeout +agent_executor = AgentExecutor( + agent=agent, + tools=[slow_tool], + callbacks=[TimeoutCallback(timeout=30)], # 30 second timeout + max_iterations=10 +) +``` + +### Retry logic + +```python +from langchain.callbacks import RetryCallback + +# Retry on failure +agent_executor = AgentExecutor( + agent=agent, + tools=[unreliable_tool], + callbacks=[RetryCallback(max_retries=3)], + max_execution_time=60 +) +``` + +## Advanced patterns + +### Dynamic tool selection + +```python +# Select tools based on context +def get_tools_for_user(user_role: str): + if user_role == "admin": + return [search, calculator, database_query, delete_data] + elif user_role == "analyst": + return [search, calculator, database_query] + else: + return [search, calculator] + +# Create agent with role-based tools +tools = get_tools_for_user(current_user.role) +agent = create_agent(model=model, tools=tools) +``` + +### Multi-step reasoning + +```python +# Agent plans multiple steps +system_prompt = """Break down complex tasks into steps: +1. Analyze the question +2. Determine required information +3. Use tools to gather data +4. Synthesize findings +5. Provide final answer""" + +agent = create_agent( + model=model, + tools=[search, calculator, database], + system_prompt=system_prompt +) + +result = agent.invoke({ + "input": "Compare revenue growth of top 3 tech companies over 5 years" +}) +``` + +### Structured output from agents + +```python +from langchain_core.pydantic_v1 import BaseModel, Field + +class ResearchReport(BaseModel): + summary: str = Field(description="Executive summary") + findings: list[str] = Field(description="Key findings") + sources: list[str] = Field(description="Source URLs") + +# Agent returns structured output +structured_agent = agent.with_structured_output(ResearchReport) +report = structured_agent.invoke({"input": "Research AI safety"}) +print(report.summary, report.findings) +``` + +## Middleware & customization + +### Custom agent middleware + +```python +from langchain.agents import AgentExecutor + +def logging_middleware(agent_executor): + """Log all agent actions.""" + original_invoke = agent_executor.invoke + + def wrapped_invoke(*args, **kwargs): + print(f"Agent invoked with: {args[0]}") + result = original_invoke(*args, **kwargs) + print(f"Agent result: {result}") + return result + + agent_executor.invoke = wrapped_invoke + return agent_executor + +# Apply middleware +agent_executor = logging_middleware(agent_executor) +``` + +### Custom stopping conditions + +```python +from langchain.agents import EarlyStoppingMethod + +# Stop early if confident +agent_executor = AgentExecutor( + agent=agent, + tools=[search], + early_stopping_method=EarlyStoppingMethod.GENERATE, # or FORCE + max_iterations=10 +) +``` + +## Best practices + +1. **Use tool-calling agents** - Fastest and most reliable +2. **Keep tool descriptions clear** - Agent needs to understand when to use each tool +3. **Add error handling** - Tools will fail, handle gracefully +4. **Set max_iterations** - Prevent infinite loops (default: 15) +5. **Enable streaming** - Better UX for long tasks +6. **Use verbose=True during dev** - See agent reasoning +7. **Test tool combinations** - Ensure tools work together +8. **Monitor with LangSmith** - Essential for production +9. **Cache tool results** - Avoid redundant API calls +10. **Version system prompts** - Track changes in behavior + +## Common pitfalls + +1. **Vague tool descriptions** - Agent won't know when to use tool +2. **Too many tools** - Agent gets confused (limit to 5-10) +3. **Tools without error handling** - One failure crashes agent +4. **Circular tool dependencies** - Agent gets stuck in loops +5. **Missing max_iterations** - Agent runs forever +6. **Poor system prompts** - Agent doesn't follow instructions + +## Debugging agents + +```python +# Enable verbose logging +agent_executor = AgentExecutor( + agent=agent, + tools=[calculator], + verbose=True, # See all steps + return_intermediate_steps=True # Get full trace +) + +result = agent_executor.invoke({"input": "Calculate 25 * 17"}) + +# Inspect intermediate steps +for step in result["intermediate_steps"]: + print(f"Action: {step[0].tool}") + print(f"Input: {step[0].tool_input}") + print(f"Output: {step[1]}") +``` + +## Resources + +- **ReAct Paper**: https://arxiv.org/abs/2210.03629 +- **LangChain Agents Docs**: https://docs.langchain.com/oss/python/langchain/agents +- **LangSmith Debugging**: https://smith.langchain.com diff --git a/.claude/skills/langchain/references/integration.md b/.claude/skills/langchain/references/integration.md new file mode 100644 index 00000000..c06e1226 --- /dev/null +++ b/.claude/skills/langchain/references/integration.md @@ -0,0 +1,562 @@ +# LangChain Integration Guide + +Integration with vector stores, LangSmith observability, and deployment. + +## Vector store integrations + +### Chroma (local, open-source) + +```python +from langchain_chroma import Chroma +from langchain_openai import OpenAIEmbeddings + +# Create vector store +vectorstore = Chroma.from_documents( + documents=docs, + embedding=OpenAIEmbeddings(), + persist_directory="./chroma_db" +) + +# Load existing store +vectorstore = Chroma( + persist_directory="./chroma_db", + embedding_function=OpenAIEmbeddings() +) + +# Add documents incrementally +vectorstore.add_documents([new_doc1, new_doc2]) + +# Delete documents +vectorstore.delete(ids=["doc1", "doc2"]) +``` + +### Pinecone (cloud, scalable) + +```python +from langchain_pinecone import PineconeVectorStore +import pinecone + +# Initialize Pinecone +pinecone.init(api_key="your-api-key", environment="us-west1-gcp") + +# Create index (one-time) +pinecone.create_index("my-index", dimension=1536, metric="cosine") + +# Create vector store +vectorstore = PineconeVectorStore.from_documents( + documents=docs, + embedding=OpenAIEmbeddings(), + index_name="my-index" +) + +# Query with metadata filters +results = vectorstore.similarity_search( + "Python tutorials", + k=4, + filter={"category": "beginner"} +) +``` + +### FAISS (fast similarity search) + +```python +from langchain_community.vectorstores import FAISS + +# Create FAISS index +vectorstore = FAISS.from_documents(docs, OpenAIEmbeddings()) + +# Save to disk +vectorstore.save_local("./faiss_index") + +# Load from disk +vectorstore = FAISS.load_local( + "./faiss_index", + OpenAIEmbeddings(), + allow_dangerous_deserialization=True +) + +# Merge multiple indices +vectorstore1 = FAISS.load_local("./index1", embeddings) +vectorstore2 = FAISS.load_local("./index2", embeddings) +vectorstore1.merge_from(vectorstore2) +``` + +### Weaviate (production, ML-native) + +```python +from langchain_weaviate import WeaviateVectorStore +import weaviate + +# Connect to Weaviate +client = weaviate.Client("http://localhost:8080") + +# Create vector store +vectorstore = WeaviateVectorStore.from_documents( + documents=docs, + embedding=OpenAIEmbeddings(), + client=client, + index_name="LangChain" +) + +# Hybrid search (vector + keyword) +results = vectorstore.similarity_search( + "Python async", + k=4, + alpha=0.5 # 0=keyword, 1=vector, 0.5=hybrid +) +``` + +### Qdrant (fast, open-source) + +```python +from langchain_qdrant import QdrantVectorStore +from qdrant_client import QdrantClient + +# Connect to Qdrant +client = QdrantClient(host="localhost", port=6333) + +# Create vector store +vectorstore = QdrantVectorStore.from_documents( + documents=docs, + embedding=OpenAIEmbeddings(), + collection_name="my_documents", + client=client +) +``` + +## LangSmith observability + +### Enable tracing + +```python +import os + +# Set environment variables +os.environ["LANGCHAIN_TRACING_V2"] = "true" +os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key" +os.environ["LANGCHAIN_PROJECT"] = "my-project" + +# All chains/agents automatically traced +from langchain.agents import create_agent +from langchain_anthropic import ChatAnthropic + +agent = create_agent( + model=ChatAnthropic(model="claude-sonnet-4-5-20250929"), + tools=[calculator, search] +) + +# Run - automatically logged to LangSmith +result = agent.invoke({"input": "What is 25 * 17?"}) + +# View traces at https://smith.langchain.com +``` + +### Custom metadata + +```python +from langchain.callbacks import tracing_v2_enabled + +# Add custom metadata to traces +with tracing_v2_enabled( + project_name="my-project", + tags=["production", "customer-support"], + metadata={"user_id": "12345", "session_id": "abc"} +): + result = agent.invoke({"input": "Help me with Python"}) +``` + +### Evaluate runs + +```python +from langsmith import Client + +client = Client() + +# Create dataset +dataset = client.create_dataset("qa-eval") +client.create_example( + dataset_id=dataset.id, + inputs={"question": "What is Python?"}, + outputs={"answer": "Python is a programming language"} +) + +# Evaluate +from langchain.evaluation import load_evaluator + +evaluator = load_evaluator("qa") +results = client.evaluate( + lambda x: qa_chain(x), + data=dataset, + evaluators=[evaluator] +) +``` + +## Deployment patterns + +### FastAPI server + +```python +from fastapi import FastAPI +from pydantic import BaseModel +from langchain.agents import create_agent + +app = FastAPI() + +# Initialize agent once +agent = create_agent( + model=llm, + tools=[search, calculator] +) + +class Query(BaseModel): + input: str + +@app.post("/chat") +async def chat(query: Query): + result = agent.invoke({"input": query.input}) + return {"response": result["output"]} + +# Run: uvicorn main:app --reload +``` + +### Streaming responses + +```python +from fastapi.responses import StreamingResponse +from langchain.callbacks import AsyncIteratorCallbackHandler + +@app.post("/chat/stream") +async def chat_stream(query: Query): + callback = AsyncIteratorCallbackHandler() + + async def generate(): + async for token in agent.astream({"input": query.input}): + if "output" in token: + yield token["output"] + + return StreamingResponse(generate(), media_type="text/plain") +``` + +### Docker deployment + +```dockerfile +# Dockerfile +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +```bash +# Build and run +docker build -t langchain-app . +docker run -p 8000:8000 \ + -e OPENAI_API_KEY=your-key \ + -e LANGCHAIN_API_KEY=your-key \ + langchain-app +``` + +### Kubernetes deployment + +```yaml +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: langchain-app +spec: + replicas: 3 + selector: + matchLabels: + app: langchain + template: + metadata: + labels: + app: langchain + spec: + containers: + - name: langchain + image: your-registry/langchain-app:latest + ports: + - containerPort: 8000 + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: langchain-secrets + key: openai-api-key + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" +``` + +## Model integrations + +### OpenAI + +```python +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI( + model="gpt-4o", + temperature=0, + max_tokens=1000, + timeout=30, + max_retries=2 +) +``` + +### Anthropic + +```python +from langchain_anthropic import ChatAnthropic + +llm = ChatAnthropic( + model="claude-sonnet-4-5-20250929", + temperature=0, + max_tokens=4096, + timeout=60 +) +``` + +### Google + +```python +from langchain_google_genai import ChatGoogleGenerativeAI + +llm = ChatGoogleGenerativeAI( + model="gemini-2.0-flash-exp", + temperature=0 +) +``` + +### Local models (Ollama) + +```python +from langchain_community.llms import Ollama + +llm = Ollama( + model="llama3", + base_url="http://localhost:11434" +) +``` + +### Azure OpenAI + +```python +from langchain_openai import AzureChatOpenAI + +llm = AzureChatOpenAI( + azure_endpoint="https://your-endpoint.openai.azure.com/", + azure_deployment="gpt-4", + api_version="2024-02-15-preview" +) +``` + +## Tool integrations + +### Web search + +```python +from langchain_community.tools import DuckDuckGoSearchRun, TavilySearchResults + +# DuckDuckGo (free) +search = DuckDuckGoSearchRun() + +# Tavily (best quality) +search = TavilySearchResults(api_key="your-key") +``` + +### Wikipedia + +```python +from langchain_community.tools import WikipediaQueryRun +from langchain_community.utilities import WikipediaAPIWrapper + +wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper()) +``` + +### Python REPL + +```python +from langchain_experimental.tools import PythonREPLTool + +python_repl = PythonREPLTool() + +# Agent can execute Python code +agent = create_agent(model=llm, tools=[python_repl]) +result = agent.invoke({"input": "Calculate the 10th Fibonacci number"}) +``` + +### Shell commands + +```python +from langchain_community.tools import ShellTool + +shell = ShellTool() + +# Agent can run shell commands +agent = create_agent(model=llm, tools=[shell]) +``` + +### SQL databases + +```python +from langchain_community.utilities import SQLDatabase +from langchain_community.agent_toolkits import create_sql_agent + +db = SQLDatabase.from_uri("sqlite:///mydatabase.db") + +agent = create_sql_agent( + llm=llm, + db=db, + agent_type="openai-tools", + verbose=True +) + +result = agent.run("How many users are in the database?") +``` + +## Memory integrations + +### Redis + +```python +from langchain.memory import RedisChatMessageHistory +from langchain.memory import ConversationBufferMemory + +# Redis-backed memory +message_history = RedisChatMessageHistory( + url="redis://localhost:6379", + session_id="user-123" +) + +memory = ConversationBufferMemory( + chat_memory=message_history, + return_messages=True +) +``` + +### PostgreSQL + +```python +from langchain_postgres import PostgresChatMessageHistory + +message_history = PostgresChatMessageHistory( + connection_string="postgresql://user:pass@localhost/db", + session_id="user-123" +) +``` + +### MongoDB + +```python +from langchain_mongodb import MongoDBChatMessageHistory + +message_history = MongoDBChatMessageHistory( + connection_string="mongodb://localhost:27017/", + session_id="user-123" +) +``` + +## Caching + +### In-memory cache + +```python +from langchain.cache import InMemoryCache +from langchain.globals import set_llm_cache + +set_llm_cache(InMemoryCache()) + +# Same query uses cache +response1 = llm.invoke("What is Python?") # API call +response2 = llm.invoke("What is Python?") # Cached +``` + +### SQLite cache + +```python +from langchain.cache import SQLiteCache + +set_llm_cache(SQLiteCache(database_path=".langchain.db")) +``` + +### Redis cache + +```python +from langchain.cache import RedisCache +from redis import Redis + +set_llm_cache(RedisCache(redis_=Redis(host="localhost", port=6379))) +``` + +## Monitoring & logging + +### Custom callbacks + +```python +from langchain.callbacks.base import BaseCallbackHandler + +class CustomCallback(BaseCallbackHandler): + def on_llm_start(self, serialized, prompts, **kwargs): + print(f"LLM started with prompts: {prompts}") + + def on_llm_end(self, response, **kwargs): + print(f"LLM finished with: {response}") + + def on_tool_start(self, serialized, input_str, **kwargs): + print(f"Tool {serialized['name']} started with: {input_str}") + + def on_tool_end(self, output, **kwargs): + print(f"Tool finished with: {output}") + +# Use callback +agent = create_agent( + model=llm, + tools=[calculator], + callbacks=[CustomCallback()] +) +``` + +### Token counting + +```python +from langchain.callbacks import get_openai_callback + +with get_openai_callback() as cb: + result = llm.invoke("Write a long story") + print(f"Tokens used: {cb.total_tokens}") + print(f"Cost: ${cb.total_cost:.4f}") +``` + +## Best practices + +1. **Use LangSmith in production** - Essential for debugging +2. **Cache aggressively** - LLM calls are expensive +3. **Set timeouts** - Prevent hanging requests +4. **Add retries** - Handle transient failures +5. **Monitor costs** - Track token usage +6. **Version your prompts** - Track changes +7. **Use async** - Better performance for I/O +8. **Persistent memory** - Don't lose conversation history +9. **Secure API keys** - Use environment variables +10. **Test integrations** - Verify connections before production + +## Resources + +- **LangSmith**: https://smith.langchain.com +- **Vector Stores**: https://python.langchain.com/docs/integrations/vectorstores +- **Model Providers**: https://python.langchain.com/docs/integrations/llms +- **Tools**: https://python.langchain.com/docs/integrations/tools +- **Deployment Guide**: https://docs.langchain.com/deploy diff --git a/.claude/skills/langchain/references/rag.md b/.claude/skills/langchain/references/rag.md new file mode 100644 index 00000000..294e7895 --- /dev/null +++ b/.claude/skills/langchain/references/rag.md @@ -0,0 +1,600 @@ +# LangChain RAG Guide + +Complete guide to Retrieval-Augmented Generation with LangChain. + +## What is RAG? + +**RAG (Retrieval-Augmented Generation)** combines: +1. **Retrieval**: Find relevant documents from knowledge base +2. **Generation**: LLM generates answer using retrieved context + +**Benefits**: +- Reduce hallucinations +- Up-to-date information +- Domain-specific knowledge +- Source citations + +## RAG pipeline components + +### 1. Document loading + +```python +from langchain_community.document_loaders import ( + WebBaseLoader, + PyPDFLoader, + TextLoader, + DirectoryLoader, + CSVLoader, + UnstructuredMarkdownLoader +) + +# Web pages +loader = WebBaseLoader("https://docs.python.org/3/tutorial/") +docs = loader.load() + +# PDF files +loader = PyPDFLoader("paper.pdf") +docs = loader.load() + +# Multiple PDFs +loader = DirectoryLoader("./papers/", glob="**/*.pdf", loader_cls=PyPDFLoader) +docs = loader.load() + +# Text files +loader = TextLoader("data.txt") +docs = loader.load() + +# CSV +loader = CSVLoader("data.csv") +docs = loader.load() + +# Markdown +loader = UnstructuredMarkdownLoader("README.md") +docs = loader.load() +``` + +### 2. Text splitting + +```python +from langchain.text_splitter import ( + RecursiveCharacterTextSplitter, + CharacterTextSplitter, + TokenTextSplitter +) + +# Recommended: Recursive (tries multiple separators) +text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, # Characters per chunk + chunk_overlap=200, # Overlap between chunks + length_function=len, + separators=["\n\n", "\n", " ", ""] +) + +splits = text_splitter.split_documents(docs) + +# Token-based (for precise token limits) +text_splitter = TokenTextSplitter( + chunk_size=512, # Tokens per chunk + chunk_overlap=50 +) + +# Character-based (simple) +text_splitter = CharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200, + separator="\n\n" +) +``` + +**Chunk size recommendations**: +- **Short answers**: 256-512 tokens +- **General Q&A**: 512-1024 tokens (recommended) +- **Long context**: 1024-2048 tokens +- **Overlap**: 10-20% of chunk_size + +### 3. Embeddings + +```python +from langchain_openai import OpenAIEmbeddings +from langchain_community.embeddings import ( + HuggingFaceEmbeddings, + CohereEmbeddings +) + +# OpenAI (fast, high quality) +embeddings = OpenAIEmbeddings(model="text-embedding-3-small") + +# HuggingFace (free, local) +embeddings = HuggingFaceEmbeddings( + model_name="sentence-transformers/all-mpnet-base-v2" +) + +# Cohere +embeddings = CohereEmbeddings(model="embed-english-v3.0") +``` + +### 4. Vector stores + +```python +from langchain_chroma import Chroma +from langchain_community.vectorstores import FAISS +from langchain_pinecone import PineconeVectorStore + +# Chroma (local, persistent) +vectorstore = Chroma.from_documents( + documents=splits, + embedding=embeddings, + persist_directory="./chroma_db" +) + +# FAISS (fast similarity search) +vectorstore = FAISS.from_documents(splits, embeddings) +vectorstore.save_local("./faiss_index") + +# Pinecone (cloud, scalable) +vectorstore = PineconeVectorStore.from_documents( + documents=splits, + embedding=embeddings, + index_name="my-index" +) +``` + +### 5. Retrieval + +```python +# Basic retriever (top-k similarity) +retriever = vectorstore.as_retriever( + search_type="similarity", + search_kwargs={"k": 4} # Return top 4 documents +) + +# MMR (Maximal Marginal Relevance) - diverse results +retriever = vectorstore.as_retriever( + search_type="mmr", + search_kwargs={ + "k": 4, + "fetch_k": 20, # Fetch 20, return diverse 4 + "lambda_mult": 0.5 # Diversity (0=diverse, 1=similar) + } +) + +# Similarity score threshold +retriever = vectorstore.as_retriever( + search_type="similarity_score_threshold", + search_kwargs={ + "score_threshold": 0.5 # Minimum similarity score + } +) + +# Query documents directly +docs = retriever.get_relevant_documents("What is Python?") +``` + +### 6. QA chain + +```python +from langchain.chains import RetrievalQA +from langchain_anthropic import ChatAnthropic + +llm = ChatAnthropic(model="claude-sonnet-4-5-20250929") + +# Basic QA chain +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + return_source_documents=True +) + +# Query +result = qa_chain({"query": "What are Python decorators?"}) +print(result["result"]) +print(f"Sources: {len(result['source_documents'])}") +``` + +## Advanced RAG patterns + +### Conversational RAG + +```python +from langchain.chains import ConversationalRetrievalChain +from langchain.memory import ConversationBufferMemory + +# Add memory +memory = ConversationBufferMemory( + memory_key="chat_history", + return_messages=True, + output_key="answer" +) + +# Conversational RAG chain +qa = ConversationalRetrievalChain.from_llm( + llm=llm, + retriever=retriever, + memory=memory, + return_source_documents=True +) + +# Multi-turn conversation +result1 = qa({"question": "What is Python used for?"}) +result2 = qa({"question": "Can you give examples?"}) # Remembers context +result3 = qa({"question": "What about web development?"}) +``` + +### Custom prompt template + +```python +from langchain.prompts import PromptTemplate + +# Custom QA prompt +template = """Use the following pieces of context to answer the question. +If you don't know the answer, say so - don't make it up. +Always cite your sources using [Source N] notation. + +Context: {context} + +Question: {question} + +Helpful Answer:""" + +prompt = PromptTemplate( + template=template, + input_variables=["context", "question"] +) + +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + chain_type_kwargs={"prompt": prompt} +) +``` + +### Chain types + +```python +# 1. Stuff (default) - Put all docs in context +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + chain_type="stuff" # Fast, works if docs fit in context +) + +# 2. Map-reduce - Summarize each doc, then combine +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + chain_type="map_reduce" # For many documents +) + +# 3. Refine - Iteratively refine answer +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + chain_type="refine" # Most thorough, slowest +) + +# 4. Map-rerank - Score answers, return best +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + chain_type="map_rerank" # Good for multiple perspectives +) +``` + +### Multi-query retrieval + +```python +from langchain.retrievers import MultiQueryRetriever + +# Generate multiple queries for better recall +retriever = MultiQueryRetriever.from_llm( + retriever=vectorstore.as_retriever(), + llm=llm +) + +# "What is Python?" becomes: +# - "What is Python programming language?" +# - "Python language definition" +# - "Overview of Python" +docs = retriever.get_relevant_documents("What is Python?") +``` + +### Contextual compression + +```python +from langchain.retrievers import ContextualCompressionRetriever +from langchain.retrievers.document_compressors import LLMChainExtractor + +# Compress retrieved docs to relevant parts only +compressor = LLMChainExtractor.from_llm(llm) + +compression_retriever = ContextualCompressionRetriever( + base_compressor=compressor, + base_retriever=vectorstore.as_retriever() +) + +# Returns only relevant excerpts +compressed_docs = compression_retriever.get_relevant_documents("Python decorators") +``` + +### Ensemble retrieval (hybrid search) + +```python +from langchain.retrievers import EnsembleRetriever +from langchain.retrievers import BM25Retriever + +# Vector search (semantic) +vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) + +# Keyword search (BM25) +keyword_retriever = BM25Retriever.from_documents(splits) +keyword_retriever.k = 5 + +# Combine both +ensemble_retriever = EnsembleRetriever( + retrievers=[vector_retriever, keyword_retriever], + weights=[0.5, 0.5] # Equal weight +) + +docs = ensemble_retriever.get_relevant_documents("Python async") +``` + +## RAG with agents + +### Agent-based RAG + +```python +from langchain.agents import create_tool_calling_agent +from langchain.tools.retriever import create_retriever_tool + +# Create retriever tool +retriever_tool = create_retriever_tool( + retriever=retriever, + name="python_docs", + description="Searches Python documentation for answers about Python programming" +) + +# Create agent with retriever tool +agent = create_tool_calling_agent( + llm=llm, + tools=[retriever_tool, calculator, search], + system_prompt="Use python_docs tool for Python questions" +) + +# Agent decides when to retrieve +from langchain.agents import AgentExecutor +agent_executor = AgentExecutor(agent=agent, tools=[retriever_tool]) + +result = agent_executor.invoke({"input": "What are Python generators?"}) +``` + +### Multi-document agents + +```python +# Multiple knowledge bases +python_retriever = create_retriever_tool( + retriever=python_vectorstore.as_retriever(), + name="python_docs", + description="Python programming documentation" +) + +numpy_retriever = create_retriever_tool( + retriever=numpy_vectorstore.as_retriever(), + name="numpy_docs", + description="NumPy library documentation" +) + +# Agent chooses which knowledge base to query +agent = create_agent( + model=llm, + tools=[python_retriever, numpy_retriever, search] +) + +result = agent.invoke({"input": "How do I create numpy arrays?"}) +``` + +## Metadata filtering + +### Add metadata to documents + +```python +from langchain.schema import Document + +# Documents with metadata +docs = [ + Document( + page_content="Python is a programming language", + metadata={"source": "tutorial.pdf", "page": 1, "category": "intro"} + ), + Document( + page_content="Python decorators modify functions", + metadata={"source": "advanced.pdf", "page": 42, "category": "advanced"} + ) +] + +vectorstore = Chroma.from_documents(docs, embeddings) +``` + +### Filter by metadata + +```python +# Retrieve only from specific source +retriever = vectorstore.as_retriever( + search_kwargs={ + "k": 4, + "filter": {"category": "intro"} # Only intro documents + } +) + +# Multiple filters +retriever = vectorstore.as_retriever( + search_kwargs={ + "k": 4, + "filter": { + "category": "advanced", + "source": "advanced.pdf" + } + } +) +``` + +## Document preprocessing + +### Clean documents + +```python +def preprocess_doc(doc): + """Clean and normalize document.""" + # Remove extra whitespace + doc.page_content = " ".join(doc.page_content.split()) + + # Remove special characters + doc.page_content = re.sub(r'[^\w\s]', '', doc.page_content) + + # Lowercase (optional) + doc.page_content = doc.page_content.lower() + + return doc + +# Apply preprocessing +clean_docs = [preprocess_doc(doc) for doc in docs] +``` + +### Extract structured data + +```python +from langchain.document_transformers import Html2TextTransformer + +# HTML to clean text +transformer = Html2TextTransformer() +clean_docs = transformer.transform_documents(html_docs) + +# Extract tables +from langchain.document_loaders import UnstructuredHTMLLoader + +loader = UnstructuredHTMLLoader("data.html") +docs = loader.load() # Extracts tables as structured data +``` + +## Evaluation & monitoring + +### Evaluate retrieval quality + +```python +from langchain.evaluation import load_evaluator + +# Relevance evaluator +evaluator = load_evaluator("relevance", llm=llm) + +# Test retrieval +query = "What are Python decorators?" +retrieved_docs = retriever.get_relevant_documents(query) + +for doc in retrieved_docs: + result = evaluator.evaluate_strings( + input=query, + prediction=doc.page_content + ) + print(f"Relevance score: {result['score']}") +``` + +### Track sources + +```python +# Always return sources +qa_chain = RetrievalQA.from_chain_type( + llm=llm, + retriever=retriever, + return_source_documents=True +) + +result = qa_chain({"query": "What is Python?"}) + +# Show sources to user +print(result["result"]) +print("\nSources:") +for i, doc in enumerate(result["source_documents"]): + print(f"[{i+1}] {doc.metadata.get('source', 'Unknown')}") + print(f" {doc.page_content[:100]}...") +``` + +## Best practices + +1. **Chunk size matters** - 512-1024 tokens is usually optimal +2. **Add overlap** - 10-20% overlap prevents context loss +3. **Use metadata** - Track sources for citations +4. **Test retrieval quality** - Evaluate before using in production +5. **Hybrid search** - Combine vector + keyword for best results +6. **Compress context** - Remove irrelevant parts before LLM +7. **Cache embeddings** - Expensive, cache when possible +8. **Version your index** - Track changes to knowledge base +9. **Monitor failures** - Log when retrieval doesn't find answers +10. **Update regularly** - Keep knowledge base current + +## Common pitfalls + +1. **Chunks too large** - Won't fit in context +2. **No overlap** - Important context lost at boundaries +3. **No metadata** - Can't cite sources +4. **Poor splitting** - Breaks mid-sentence or mid-paragraph +5. **Wrong embedding model** - Domain mismatch hurts retrieval +6. **No reranking** - Lower quality results +7. **Ignoring failures** - No handling when retrieval fails + +## Performance optimization + +### Caching + +```python +from langchain.cache import InMemoryCache, SQLiteCache +from langchain.globals import set_llm_cache + +# In-memory cache +set_llm_cache(InMemoryCache()) + +# Persistent cache +set_llm_cache(SQLiteCache(database_path=".langchain.db")) + +# Same query uses cache (faster + cheaper) +result1 = qa_chain({"query": "What is Python?"}) +result2 = qa_chain({"query": "What is Python?"}) # Cached +``` + +### Batch processing + +```python +# Process multiple queries efficiently +queries = [ + "What is Python?", + "What are decorators?", + "How do I use async?" +] + +# Batch retrieval +all_docs = vectorstore.similarity_search_batch(queries) + +# Batch QA +results = qa_chain.batch([{"query": q} for q in queries]) +``` + +### Async operations + +```python +# Async RAG for concurrent queries +import asyncio + +async def async_qa(query): + return await qa_chain.ainvoke({"query": query}) + +# Run multiple queries concurrently +results = await asyncio.gather( + async_qa("What is Python?"), + async_qa("What are decorators?") +) +``` + +## Resources + +- **LangChain RAG Docs**: https://docs.langchain.com/oss/python/langchain/rag +- **Vector Stores**: https://python.langchain.com/docs/integrations/vectorstores +- **Document Loaders**: https://python.langchain.com/docs/integrations/document_loaders +- **Retrievers**: https://python.langchain.com/docs/modules/data_connection/retrievers diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 00000000..e215bc4c --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts new file mode 100644 index 00000000..2f8959a1 --- /dev/null +++ b/app/(auth)/auth/callback/route.ts @@ -0,0 +1,35 @@ +import { createClient } from '@/lib/supabase/server' +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url) + const code = searchParams.get('code') + const next = searchParams.get('next') ?? '/' + + if (code) { + const supabase = await createClient() + const { error } = await supabase.auth.exchangeCodeForSession(code) + + if (!error) { + // Check if user has completed onboarding + const { data: { user } } = await supabase.auth.getUser() + + if (user) { + const { data: settings } = await supabase + .from('company_settings') + .select('onboarding_complete') + .eq('user_id', user.id) + .single() + + if (!settings?.onboarding_complete) { + return NextResponse.redirect(`${origin}/onboarding`) + } + } + + return NextResponse.redirect(`${origin}${next}`) + } + } + + // Return the user to an error page with instructions + return NextResponse.redirect(`${origin}/login?error=auth_error`) +} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 00000000..b83fcd1a --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,144 @@ +'use client' + +import { useState } from 'react' +import { createClient } from '@/lib/supabase/client' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { useToast } from '@/components/ui/use-toast' +import { Loader2, Mail, Sparkles } from 'lucide-react' + +export default function LoginPage() { + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isEmailSent, setIsEmailSent] = useState(false) + const { toast } = useToast() + const supabase = createClient() + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + + try { + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { + emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL || window.location.origin}/auth/callback`, + }, + }) + + if (error) { + toast({ + title: 'Fel', + description: error.message, + variant: 'destructive', + }) + return + } + + setIsEmailSent(true) + toast({ + title: 'E-post skickad!', + description: 'Kolla din inkorg för att logga in.', + }) + } catch { + toast({ + title: 'Fel', + description: 'Något gick fel. Försök igen.', + variant: 'destructive', + }) + } finally { + setIsLoading(false) + } + } + + if (isEmailSent) { + return ( +
+ + +
+ +
+ Kolla din e-post + + Vi har skickat en inloggningslänk till {email} + +
+ +

+ Klicka på länken i e-posten för att logga in. Länken är giltig i 1 timme. +

+ +
+
+
+ ) + } + + return ( +
+ + +
+ +
+ Influencer Assistant + + Logga in med din e-post för att hantera din verksamhet + +
+ +
+
+ + setEmail(e.target.value)} + required + disabled={isLoading} + /> +
+ +
+

+ Genom att logga in godkänner du våra{' '} + + villkor + {' '} + och{' '} + + integritetspolicy + + . +

+
+
+
+ ) +} diff --git a/app/(dashboard)/analytics/page.tsx b/app/(dashboard)/analytics/page.tsx new file mode 100644 index 00000000..6a58d0a4 --- /dev/null +++ b/app/(dashboard)/analytics/page.tsx @@ -0,0 +1,333 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { createClient } from '@/lib/supabase/client' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + TikTokConnectButton, + TikTokAccountCard, + TikTokStatsWidget, + TikTokGrowthChart, + TikTokVideoList, + VideoLinkModal, + TikTokROITable, +} from '@/components/tiktok' +import type { TikTokAccount, TikTokStatsSummary, TikTokVideo, TikTokCampaignROI } from '@/types' +import { + Loader2, + TrendingUp, + Video, + Target, + BarChart3, +} from 'lucide-react' + +export default function AnalyticsPage() { + const router = useRouter() + const supabase = createClient() + + const [isLoading, setIsLoading] = useState(true) + const [accounts, setAccounts] = useState([]) + const [stats, setStats] = useState(null) + const [roiData, setRoiData] = useState([]) + const [selectedVideo, setSelectedVideo] = useState(null) + const [isLinkModalOpen, setIsLinkModalOpen] = useState(false) + const [isSyncing, setIsSyncing] = useState(false) + + useEffect(() => { + checkAuth() + }, []) + + const checkAuth = async () => { + const { data: { user } } = await supabase.auth.getUser() + if (!user) { + router.push('/login') + return + } + fetchData() + } + + const fetchData = async () => { + setIsLoading(true) + await Promise.all([ + fetchAccounts(), + fetchStats(), + fetchROI(), + ]) + setIsLoading(false) + } + + const fetchAccounts = async () => { + try { + const response = await fetch('/api/tiktok/accounts') + const data = await response.json() + setAccounts(data.accounts || []) + } catch (error) { + console.error('Failed to fetch accounts:', error) + } + } + + const fetchStats = async () => { + try { + const response = await fetch('/api/tiktok/stats') + const data = await response.json() + setStats(data.summary || null) + } catch (error) { + console.error('Failed to fetch stats:', error) + } + } + + const fetchROI = async () => { + // Fetch campaigns with TikTok videos for ROI calculation + try { + const response = await fetch('/api/tiktok/videos?limit=100') + const data = await response.json() + + // Group videos by campaign and calculate ROI + // This is a simplified version - the actual calculation is in the API + const campaignVideos = new Map() + for (const video of data.videos || []) { + if (video.campaign_id) { + if (!campaignVideos.has(video.campaign_id)) { + campaignVideos.set(video.campaign_id, []) + } + campaignVideos.get(video.campaign_id)!.push(video) + } + } + + // For now, just set empty - actual ROI data would come from a dedicated endpoint + setRoiData([]) + } catch (error) { + console.error('Failed to fetch ROI data:', error) + } + } + + const handleSync = async () => { + if (accounts.length === 0) return + + setIsSyncing(true) + try { + const activeAccount = accounts.find(a => a.status === 'active') + if (activeAccount) { + await fetch('/api/tiktok/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_id: activeAccount.id, sync_type: 'full' }), + }) + await fetchData() + } + } catch (error) { + console.error('Sync failed:', error) + } + setIsSyncing(false) + } + + const handleVideoLinkClick = (video: TikTokVideo) => { + setSelectedVideo(video) + setIsLinkModalOpen(true) + } + + if (isLoading) { + return ( +
+ +
+ ) + } + + const activeAccount = accounts.find(a => a.status === 'active') + + return ( +
+
+

Analytics

+

+ Analysera din sociala medieprestanda och kampanj-ROI +

+
+ + {/* No connected account */} + {accounts.length === 0 && ( + + + Koppla TikTok + + Anslut ditt TikTok-konto för att se statistik och analysera kampanjprestanda + + + + + + + )} + + {/* Connected account */} + {activeAccount && ( + <> + {/* Stats overview */} +
+ + +
+ + Följare +
+

+ {stats?.currentFollowers.toLocaleString('sv-SE') || '0'} +

+ {stats?.followerChange7d !== undefined && ( +

= 0 ? 'text-success' : 'text-destructive'}`}> + {stats.followerChange7d >= 0 ? '+' : ''}{stats.followerChange7d.toLocaleString('sv-SE')} senaste 7 dagar +

+ )} +
+
+ + + +
+
+

+ {stats?.totalVideos || 0} +

+

+ {stats?.totalLikes.toLocaleString('sv-SE') || '0'} totala likes +

+
+
+ + + +
+ + Engagement Rate +
+

+ {stats?.engagementRate.toFixed(1) || '0'}% +

+

+ Genomsnitt senaste videor +

+
+
+ + + +
+ + 30-dagars tillväxt +
+

+ {stats?.followerChange30d !== undefined ? ( + <> + {stats.followerChange30d >= 0 ? '+' : ''} + {stats.followerChange30d.toLocaleString('sv-SE')} + + ) : '0'} +

+

+ nya följare +

+
+
+
+ + {/* Tabs for different views */} + + + Tillväxt + Videor + Kampanj-ROI + + + + + + {/* Recent videos with metrics */} + + + Senaste videor + + Prestanda för dina senaste publiceringar + + + + + + + + + + + +
+
+ Alla videor + + Klicka på länk-ikonen för att koppla en video till en kampanj + +
+
+
+ + + +
+
+ + + + + Kampanj-ROI + + Analysera avkastningen på dina influencer-kampanjer baserat på TikTok-prestanda + + + + + + + +
+ + {/* Account info at bottom */} + + + Kopplat konto + + + + + + + )} + + {/* Video link modal */} + { + setIsLinkModalOpen(false) + setSelectedVideo(null) + }} + onSuccess={fetchData} + /> +
+ ) +} diff --git a/app/(dashboard)/bookkeeping/page.tsx b/app/(dashboard)/bookkeeping/page.tsx new file mode 100644 index 00000000..27d73023 --- /dev/null +++ b/app/(dashboard)/bookkeeping/page.tsx @@ -0,0 +1,42 @@ +'use client' + +import { useState } from 'react' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' +import JournalEntryList from '@/components/bookkeeping/JournalEntryList' +import JournalEntryForm from '@/components/bookkeeping/JournalEntryForm' +import ChartOfAccounts from '@/components/bookkeeping/ChartOfAccounts' + +export default function BookkeepingPage() { + const [refreshKey, setRefreshKey] = useState(0) + + return ( +
+
+

Bokföring

+

+ Verifikationer, kontoplan och manuella bokföringsorder +

+
+ + + + Verifikationer + Ny verifikation + Kontoplan + + + + + + + + setRefreshKey((k) => k + 1)} /> + + + + + + +
+ ) +} diff --git a/app/(dashboard)/calendar/page.tsx b/app/(dashboard)/calendar/page.tsx new file mode 100644 index 00000000..8759de79 --- /dev/null +++ b/app/(dashboard)/calendar/page.tsx @@ -0,0 +1,142 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { createClient } from '@/lib/supabase/client' +import { useToast } from '@/components/ui/use-toast' +import { PaymentCalendar } from '@/components/calendar/PaymentCalendar' +import type { Invoice, Deadline } from '@/types' + +export default function CalendarPage() { + const [invoices, setInvoices] = useState([]) + const [deadlines, setDeadlines] = useState([]) + const [customers, setCustomers] = useState<{ id: string; name: string }[]>([]) + const [isLoading, setIsLoading] = useState(true) + const { toast } = useToast() + const supabase = createClient() + + const fetchData = useCallback(async () => { + setIsLoading(true) + + try { + // Fetch invoices with customer names + const { data: invoicesData, error: invoicesError } = await supabase + .from('invoices') + .select('*, customer:customers(name)') + .order('due_date', { ascending: true }) + + if (invoicesError) throw invoicesError + + // Fetch deadlines with customer names + const { data: deadlinesData, error: deadlinesError } = await supabase + .from('deadlines') + .select('*, customer:customers(name)') + .order('due_date', { ascending: true }) + + if (deadlinesError) throw deadlinesError + + // Fetch customers for the form + const { data: customersData, error: customersError } = await supabase + .from('customers') + .select('id, name') + .order('name', { ascending: true }) + + if (customersError) throw customersError + + setInvoices(invoicesData || []) + setDeadlines(deadlinesData || []) + setCustomers(customersData || []) + } catch (error) { + toast({ + title: 'Fel', + description: 'Kunde inte hämta data', + variant: 'destructive', + }) + } finally { + setIsLoading(false) + } + }, [supabase, toast]) + + useEffect(() => { + fetchData() + }, [fetchData]) + + const handleDeadlineCreate = async ( + data: Omit + ) => { + try { + const { error } = await supabase.from('deadlines').insert([data]) + + if (error) throw error + + toast({ + title: 'Deadline skapad', + description: 'Din deadline har sparats', + }) + + fetchData() + } catch (error) { + toast({ + title: 'Fel', + description: 'Kunde inte skapa deadline', + variant: 'destructive', + }) + throw error + } + } + + const handleDeadlineToggle = async (deadline: Deadline) => { + try { + const { error } = await supabase + .from('deadlines') + .update({ + is_completed: !deadline.is_completed, + completed_at: !deadline.is_completed ? new Date().toISOString() : null, + }) + .eq('id', deadline.id) + + if (error) throw error + + toast({ + title: deadline.is_completed ? 'Markerad som ej klar' : 'Markerad som klar', + }) + + fetchData() + } catch (error) { + toast({ + title: 'Fel', + description: 'Kunde inte uppdatera deadline', + variant: 'destructive', + }) + } + } + + if (isLoading) { + return ( +
+
+

Kalender

+
+
+
+
+
+
+ ) + } + + return ( +
+
+

Kalender

+
+ + +
+ ) +} diff --git a/app/(dashboard)/campaigns/[id]/page.tsx b/app/(dashboard)/campaigns/[id]/page.tsx new file mode 100644 index 00000000..72b7c3ef --- /dev/null +++ b/app/(dashboard)/campaigns/[id]/page.tsx @@ -0,0 +1,103 @@ +'use client' + +import { useState, useEffect, use } from 'react' +import { createClient } from '@/lib/supabase/client' +import { Campaign, Customer } from '@/types' +import { CampaignDetail, CampaignForm } from '@/components/campaigns' +import { useToast } from '@/components/ui/use-toast' +import { Skeleton } from '@/components/ui/skeleton' + +interface PageProps { + params: Promise<{ id: string }> +} + +export default function CampaignDetailPage({ params }: PageProps) { + const { id } = use(params) + const supabase = createClient() + const { toast } = useToast() + const [campaign, setCampaign] = useState(null) + const [customers, setCustomers] = useState([]) + const [loading, setLoading] = useState(true) + const [editFormOpen, setEditFormOpen] = useState(false) + + const fetchCampaign = async () => { + try { + const response = await fetch(`/api/campaigns/${id}`) + if (response.ok) { + const { data } = await response.json() + setCampaign(data) + } else { + toast({ + title: 'Fel', + description: 'Samarbetet hittades inte', + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: 'Fel', + description: 'Kunde inte ladda samarbetet', + variant: 'destructive', + }) + } finally { + setLoading(false) + } + } + + const fetchCustomers = async () => { + const { data } = await supabase + .from('customers') + .select('*') + .order('name') + setCustomers(data || []) + } + + useEffect(() => { + fetchCampaign() + fetchCustomers() + }, [id]) + + if (loading) { + return ( +
+ + +
+ {[1, 2, 3, 4].map(i => ( + + ))} +
+ +
+ ) + } + + if (!campaign) { + return ( +
+

Samarbetet hittades inte

+
+ ) + } + + return ( + <> + setEditFormOpen(true)} + /> + + { + setEditFormOpen(false) + fetchCampaign() + }} + /> + + ) +} diff --git a/app/(dashboard)/campaigns/import/page.tsx b/app/(dashboard)/campaigns/import/page.tsx new file mode 100644 index 00000000..95bf8f95 --- /dev/null +++ b/app/(dashboard)/campaigns/import/page.tsx @@ -0,0 +1,40 @@ +import { createClient } from '@/lib/supabase/server' +import { redirect } from 'next/navigation' +import { ContractImportWizard } from '@/components/contracts/ContractImportWizard' + +export const metadata = { + title: 'Importera avtal | Samarbeten', + description: 'Importera och analysera avtal med AI', +} + +export default async function CampaignImportPage() { + const supabase = await createClient() + + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) { + redirect('/login') + } + + // Fetch customers for matching + const { data: customers } = await supabase + .from('customers') + .select('*') + .eq('user_id', user.id) + .order('name') + + return ( +
+
+

Importera avtal

+

+ Ladda upp ett avtal och låt AI extrahera samarbetsinformation automatiskt +

+
+ + +
+ ) +} diff --git a/app/(dashboard)/campaigns/new/page.tsx b/app/(dashboard)/campaigns/new/page.tsx new file mode 100644 index 00000000..d2c17712 --- /dev/null +++ b/app/(dashboard)/campaigns/new/page.tsx @@ -0,0 +1,351 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { createClient } from '@/lib/supabase/client' +import { Customer, CreateCampaignInput, CampaignType, BillingFrequency } from '@/types' +import { + CAMPAIGN_TYPE_LABELS, + BILLING_FREQUENCY_LABELS, +} from '@/types' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { useToast } from '@/components/ui/use-toast' +import { ArrowLeft } from 'lucide-react' +import Link from 'next/link' + +const CURRENCIES = ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK'] + +export default function NewCampaignPage() { + const router = useRouter() + const supabase = createClient() + const { toast } = useToast() + const [customers, setCustomers] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const [formData, setFormData] = useState({ + name: '', + description: '', + customer_id: '', + brand_name: '', + campaign_type: 'influencer', + total_value: undefined, + currency: 'SEK', + vat_included: false, + payment_terms: 30, + billing_frequency: undefined, + publication_date: '', + draft_deadline: '', + notes: '', + }) + + useEffect(() => { + const fetchCustomers = async () => { + const { data } = await supabase + .from('customers') + .select('*') + .order('name') + setCustomers(data || []) + } + fetchCustomers() + }, []) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!formData.name) { + toast({ title: 'Namn krävs', variant: 'destructive' }) + return + } + + setIsLoading(true) + + try { + const response = await fetch('/api/campaigns', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + customer_id: formData.customer_id || null, + brand_name: formData.brand_name || null, + total_value: formData.total_value || null, + payment_terms: formData.payment_terms || null, + billing_frequency: formData.billing_frequency || null, + publication_date: formData.publication_date || null, + draft_deadline: formData.draft_deadline || null, + }), + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Failed to create campaign') + } + + const { data } = await response.json() + + toast({ + title: 'Samarbete skapat', + description: formData.name, + }) + + router.push(`/campaigns/${data.id}`) + } catch (error) { + toast({ + title: 'Fel', + description: error instanceof Error ? error.message : 'Något gick fel', + variant: 'destructive', + }) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+ + + Tillbaka till samarbeten + +

Nytt samarbete

+

+ Skapa ett nytt samarbete för att spåra innehåll, avtal och betalningar +

+
+ +
+ + + Grundläggande information + + +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="T.ex. Sommarkampanj 2025" + required + /> +
+ +
+ +