diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 36f7e57..6e8698b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,17 +2,17 @@ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], rules: { - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, -} \ No newline at end of file +}; diff --git a/README.md b/README.md index 1064dbe..f19ff4c 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,59 @@ -# Analysr - Business Analytics Platform ๐Ÿ“Š - -
- Analysr Banner - -

- Transform your business with powerful analytics and data-driven insights -

-
+# Analysr - Review Analytics Platform ๐Ÿ“Š + +![image](https://github.com/user-attachments/assets/1b26013b-25e6-4b70-a725-8a42faa91336) + +Transform your business with Analysr + +## โšก One Liner +This is my submission for Airbyte-Motherduck Hackathon - December 2024 - January 2025 + +For all you speedy folks out there, hereโ€™s the summary: + +- **1.0.0** + - With your customer reviews in Motherduck, along with your chosen business stack and areas of interest, Analysr is ready to dish out some insightful analytics. To sweeten the deal, Groq is also integrated to help you navigate all your growth phases. + - Your analytics lineup features Aspect Analysis, a Word Sentiment Heatmap (for those feelings), Advanced Text Analysis, Groq Business Analytics, Keyphrase Analysis, and a handy Competitor Comparison. + +## ๐Ÿ—๏ธ Architecture +![image](https://github.com/user-attachments/assets/0abe96f6-414a-42d2-aa0d-d0950a7da194) + + +## ๐ŸšถWalkthrough + +1) To obtain customer review insights, sync your data to Motherduck with the schema: { "review_text": "string", "stars": "number" } (More schemas will be supported soon). We recommend using Airbyte due to its extensive list of sources and seamless data movement. ![image](https://github.com/user-attachments/assets/415aece5-6594-4649-8d84-ec2fa1707988) +![image](https://github.com/user-attachments/assets/00bf63f5-952f-491a-9ffd-0241d2e2bfd2) +2) Visit the Analysr website at (growwithanalysr.web.app) and click on the "Get Started Now" button for onboarding. +![image](https://github.com/user-attachments/assets/95da4b69-29bb-4c88-9433-19865bc72093) +3) Select your business stack and substack; Groq and queries will use this information to fetch insights. +![image](https://github.com/user-attachments/assets/160c95bb-bad3-4c27-b5af-7fe651f2313c) +4) Input your Motherduck token and wait for the connection to be established (the time required will depend on your network bandwidth). +![image](https://github.com/user-attachments/assets/18d35b48-37c4-4348-8ea2-8c501a14f00a) +5) Select the database and table where your customer reviews or any related reviews exist, and set the data limit. +![image](https://github.com/user-attachments/assets/e88e07d8-1861-4f12-9dc9-672b45776509) +6) Input your Groq token (recommended) to obtain AI-based insights. +![image](https://github.com/user-attachments/assets/19178890-f24b-4d1c-ad07-f343e06c79c6) +7) Optionally provide your Airbyte bearer token (from the cloud.airbyte.com settings page) and connection ID (from the connections tab URL) to trigger a sync for updating your Motherduck table. +![image](https://github.com/user-attachments/assets/042ee2bf-8ff1-4ba3-b32c-cd720e52fb8e) +![image](https://github.com/user-attachments/assets/9f3ae847-28e2-4a93-b8a5-354b87835962) +8) Finally, input your area of interest for insights, such as customer satisfaction, and click "Continue to Dashboard."![image](https://github.com/user-attachments/assets/3c938fa2-a862-4ba6-b06e-b67bb139e71f) +9) Wait a few seconds until all queries are executed and visualized. +![image](https://github.com/user-attachments/assets/cf22aa51-cdb2-4e3f-99d6-ef93bf8f8c45) +10) Voilร ! Your dashboard will be ready, featuring all Analysr's capabilities to support your next big step! +![image](https://github.com/user-attachments/assets/1ae1427d-c315-4e02-ac75-158e3cb14d61) + +Need dataset and example method to test? +1. Hugging face dataset URL which I used, https://huggingface.co/datasets/Yelp/yelp_review_full +2. Import it to motherduck via airbyte (Set huggingface as source and motherduck as destination) +3. Get Groq token at, https://console.groq.com/keys +4. Click on continue to dashboard! That's it. Please try yourself, its fun! ## โœจ Features -### ๐ŸŽฏ Smart Business Insights -- AI-powered analysis of customer feedback and market trends -- Sentiment analysis and emotion tracking -- Key phrase extraction and topic modeling -- Competitive benchmarking and industry comparisons - -### ๐Ÿ“ˆ Advanced Analytics -- Real-time data processing and visualization -- Customizable dashboards and reports -- Trend analysis and forecasting -- Performance metrics and KPIs - -### ๐Ÿ” Deep Customer Understanding -- Customer sentiment tracking -- Demographic analysis -- Behavior pattern recognition -- Satisfaction metrics and NPS tracking - -### ๐Ÿš€ Growth Opportunities -- Market opportunity identification -- Customer pain point analysis -- Competitive advantage analysis -- Strategic recommendations +- **Aspect Analysis:** Gain insights into different aspects of customer feedback. +- **Word Sentiment Heatmap:** Visualize sentiment trends in your reviews. +- **Advanced Text Analysis:** Delve deeper into the nuances of customer language. +- **Groq Business Analytics:** Access data-driven insights to inform your growth strategy. +- **Keyphrase Analysis:** Identify and analyze key phrases that matter to your customers. +- **Competitor Comparison:** Benchmark your performance against competitors. ## ๐Ÿ› ๏ธ Technology Stack @@ -42,39 +63,38 @@ - **Visualization**: Recharts - **State Management**: Zustand - **Animations**: Framer Motion +- **Hosting**: Firebase (Production), Vercel (Experiment) +- **Proxy**: Supabase edge functions +- **CI/CD**: GitHub Actions for automated deployment + +## Future roadmap + +- **Microservice for generating queries**: Currently all queries for analytics are highly coupled with code, seperation of concerns to microservice + - [x] Create mock express server and deployed as supabase functions + - [ ] Separate DuckDB queries for as an api call + - [ ] Enhance microservice with GPT Wrapper + - [ ] Enhance business insights from Groq: Currently it hallucinates as the mixtral model is not powerful (Requires funding) ## ๐Ÿš€ Getting Started 1. Clone the repository: + ```bash -git clone https://github.com/yourusername/analysr.git +git clone https://github.com/btkcodedev/airbyte-motherduck-hackathon.git ``` 2. Install dependencies: + ```bash npm install ``` -3. Set up environment variables: -```env -MOTHERDUCK_TOKEN=your_token -GROQ_API_KEY=your_key -``` +3. Start the development server: -4. Start the development server: ```bash npm run dev ``` -## ๐Ÿ“Š Dashboard Features - -- **Real-time Analytics**: Monitor business performance in real-time -- **Sentiment Analysis**: Track customer sentiment and emotions -- **Competitive Analysis**: Compare performance against industry benchmarks -- **Text Analysis**: Extract insights from customer feedback -- **Trend Visualization**: Track metrics over time with interactive charts -- **AI Insights**: Get AI-powered recommendations for business growth - ## ๐Ÿ”’ Security - Secure token management @@ -84,7 +104,7 @@ npm run dev ## ๐Ÿ“„ License -MIT License - feel free to use this project for your own purposes! +MIT License - feel free to use this project for your purposes! ## ๐Ÿค Contributing @@ -92,10 +112,10 @@ Contributions are welcome! Please feel free to submit a Pull Request. ## ๐Ÿ“ฌ Contact -For questions and support, please open an issue or contact us at support@analysr.app +For questions and support, please open an issue or contact at btk.codedev@gmail.com ---
- Built with โค๏ธ for growing businesses -
\ No newline at end of file + Built with โค๏ธ for growing businesses by bktcodedev + diff --git a/eslint.config.js b/eslint.config.js index 82c2e20..0bbf074 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,26 +1,26 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from 'typescript-eslint'; +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, diff --git a/firebase.json b/firebase.json index 97aa404..0a0ba89 100644 --- a/firebase.json +++ b/firebase.json @@ -1,11 +1,7 @@ { "hosting": { "public": "dist", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "rewrites": [ { "source": "**", @@ -28,4 +24,4 @@ } ] } -} \ No newline at end of file +} diff --git a/index.html b/index.html index ef4901a..9c21edc 100644 --- a/index.html +++ b/index.html @@ -6,8 +6,8 @@ - Analysr - Business Analytics Platform - + Analysr + diff --git a/package-lock.json b/package-lock.json index 66c34b1..0b114ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "analysr", - "version": "0.2.0", + "version": "1.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "analysr", - "version": "0.2.0", + "version": "1.0.7", "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", @@ -34,6 +34,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.35", + "supabase": "^2.2.1", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", "vite": "^5.1.4" @@ -1025,6 +1026,19 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -2253,6 +2267,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -2439,6 +2463,23 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" }, + "node_modules/bin-links": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz", + "integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2683,6 +2724,16 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2691,6 +2742,16 @@ "node": ">=6" } }, + "node_modules/cmd-shim": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz", + "integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2956,6 +3017,16 @@ "node": ">=12" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -3469,6 +3540,40 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3617,6 +3722,19 @@ "node": ">= 12.20" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3890,6 +4008,20 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -4315,6 +4447,52 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minizlib/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/motion-dom": { "version": "11.14.3", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", @@ -4426,6 +4604,16 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4793,6 +4981,16 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4942,6 +5140,16 @@ "pify": "^2.3.0" } }, + "node_modules/read-cmd-shim": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz", + "integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5335,6 +5543,45 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supabase": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.2.1.tgz", + "integrity": "sha512-uTu8f4kT9wE3EEQTAJAWFlHyu8ymauFxWEz2FDGIQ2MzlD1Xb1NCHtsj/xvmJWqrGI1jnBYcPuatZVTHj1jR1g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bin-links": "^5.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar": "7.4.3" + }, + "bin": { + "supabase": "bin/supabase" + }, + "engines": { + "npm": ">=8" + } + }, + "node_modules/supabase/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -5416,6 +5663,34 @@ "node": ">=14.0.0" } }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5853,6 +6128,20 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/write-file-atomic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 1dd258b..93022f8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "analysr", "private": true, - "version": "0.3.0", + "version": "1.0.10", "type": "module", "scripts": { "dev": "vite", @@ -36,6 +36,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.35", + "supabase": "^2.2.1", "tailwindcss": "^3.4.1", "typescript": "^5.3.3", "vite": "^5.1.4" diff --git a/src/App.tsx b/src/App.tsx index 284471b..a37f4a3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import WelcomeScreen from "./components/index/WelcomeScreen"; -import Onboarding from "./components/index/Onboarding"; -import Dashboard from "./components/index/Dashboard"; +import WelcomeScreen from "./components/pages/WelcomeScreen"; +import Onboarding from "./components/pages/Onboarding"; +import Dashboard from "./components/pages/Dashboard"; function App() { return ( diff --git a/src/components/common/LoadingStages/constants.ts b/src/components/common/LoadingSpinner/constants.ts similarity index 100% rename from src/components/common/LoadingStages/constants.ts rename to src/components/common/LoadingSpinner/constants.ts diff --git a/src/components/dashboard/Analytics/SentimentHeatmap.tsx b/src/components/dashboard/Analytics/SentimentHeatmap.tsx index a709120..27d61e1 100644 --- a/src/components/dashboard/Analytics/SentimentHeatmap.tsx +++ b/src/components/dashboard/Analytics/SentimentHeatmap.tsx @@ -2,26 +2,14 @@ import { motion } from 'framer-motion'; import { Heart } from 'lucide-react'; import { ResponsiveContainer, Treemap, Tooltip } from 'recharts'; import TooltipComponent from '../../common/Tooltip/Tooltip'; +import NoDataFallback from '../../common/DataInfo/NoDataFallback'; -interface SentimentData { +export interface SentimentData { name: string; size: number; sentiment: number; } -const SENTIMENT_WORDS: SentimentData[] = [ - { name: 'Love', size: 800, sentiment: 1 }, - { name: 'Amazing', size: 700, sentiment: 0.9 }, - { name: 'Great', size: 600, sentiment: 0.8 }, - { name: 'Good', size: 500, sentiment: 0.6 }, - { name: 'Okay', size: 400, sentiment: 0.2 }, - { name: 'Neutral', size: 300, sentiment: 0 }, - { name: 'Poor', size: 250, sentiment: -0.4 }, - { name: 'Bad', size: 200, sentiment: -0.6 }, - { name: 'Terrible', size: 150, sentiment: -0.8 }, - { name: 'Hate', size: 100, sentiment: -1 } -]; - const getSentimentColor = (sentiment: number) => { if (sentiment > 0.5) return '#22c55e'; // Green for very positive if (sentiment > 0) return '#86efac'; // Light green for positive @@ -30,10 +18,54 @@ const getSentimentColor = (sentiment: number) => { return '#ef4444'; // Red for very negative }; -export default function SentimentHeatmap() { - const data = { +const CustomizedContent = (props: any) => { + const { x, y, width, height, name, sentiment } = props; + + return ( + + + {width > 40 && height > 25 && ( + + {name} + + )} + + ); +}; + +interface SentimentHeatmapProps { + data: SentimentData[]; +} + +export default function SentimentHeatmap({ data }: SentimentHeatmapProps) { + if (!data || data.length === 0) { + return ; + } + + const transformedData = { name: 'sentiment', - children: SENTIMENT_WORDS + children: data.map(item => ({ + name: item.name, + value: item.size, + size: item.size, + sentiment: item.sentiment + })) }; return ( @@ -48,44 +80,15 @@ export default function SentimentHeatmap() { -
- +
+ { - const { depth, x, y, width, height, name, sentiment } = props; - - if (depth === 1) { - return ( - - - {width > 30 && height > 30 && ( - - {name} - - )} - - ); - } - return null; - }} + data={transformedData.children} + dataKey="value" + aspectRatio={4/3} + stroke="#1f2937" + animationDuration={450} + content={} > { @@ -94,6 +97,9 @@ export default function SentimentHeatmap() { return (

{data.name}

+

+ Frequency: {data.size} +

Sentiment: {(data.sentiment * 100).toFixed(0)}%

@@ -106,4 +112,4 @@ export default function SentimentHeatmap() {
); -} +} \ No newline at end of file diff --git a/src/components/dashboard/DashboardView/DashboardContent.tsx b/src/components/dashboard/DashboardView/DashboardContent.tsx index 56da2fc..e5a2d4b 100644 --- a/src/components/dashboard/DashboardView/DashboardContent.tsx +++ b/src/components/dashboard/DashboardView/DashboardContent.tsx @@ -26,7 +26,6 @@ export default function DashboardContent({ if (!analyticsData || !analyticsData.totalReviews || !stack || !substack) { return ; } - return (
} > - +
import("../dashboard/Analytics/StatGrid")); -const PositiveInsights = lazy( - () => import("../dashboard/Analytics/PositiveInsights") -); -const KeyPhraseAnalysis = lazy( - () => import("../dashboard/TextAnalysis/KeyPhraseAnalysis") -); -const AspectAnalysis = lazy( - () => import("../dashboard/Analytics/AspectAnalysis") -); -const NegativeInsights = lazy( - () => import("../dashboard/Analytics/NegativeInsights") -); -const TextAnalysis = lazy(() => import("../dashboard/TextAnalysis")); -const BusinessInsights = lazy( - () => import("../dashboard/GPTInsights/BusinessInsights") -); - -interface DashboardContentProps { - analyticsData: ProcessedAnalytics | null; - stack?: string; - substack?: string; - groqToken?: string; -} - -export default function DashboardContent({ - analyticsData, - stack, - substack, - groqToken, -}: DashboardContentProps) { - if (!analyticsData || !analyticsData.totalReviews) { - return ; - } - - return ( -
- - } - > - - - - - } - > - - - - - } - > - {analyticsData?.positiveInsights && ( - - )} - - -
- - } - > - {analyticsData?.keyPhrases && ( - - )} - - - - } - > - {analyticsData?.aspectAnalysis && ( - - )} - -
- - - } - > - {analyticsData?.textAnalysis && ( - - )} - - - - } - > - {analyticsData?.negativeInsights && ( - - )} - -
- ); -} diff --git a/src/components/onboarding/CategorySelect.tsx b/src/components/onboarding/CategorySelect.tsx index 4ef1381..a76f58c 100644 --- a/src/components/onboarding/CategorySelect.tsx +++ b/src/components/onboarding/CategorySelect.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from "react"; import { motion } from "framer-motion"; import { ChevronDown } from "lucide-react"; -import type { BusinessCategory, SubStack } from "../../data/businessCategories"; +import type { BusinessCategory, SubStack } from "../../config/data/businessCategories"; interface CategorySelectProps { categories: BusinessCategory[]; diff --git a/src/components/onboarding/OnboardingForm.tsx b/src/components/onboarding/OnboardingForm.tsx index b9d8f27..0b3d7d4 100644 --- a/src/components/onboarding/OnboardingForm.tsx +++ b/src/components/onboarding/OnboardingForm.tsx @@ -4,7 +4,7 @@ import { useRef, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import CategorySelect from "./CategorySelect"; import DatabaseConnection from "./DatabaseConnection"; -import { businessCategories } from "../../data/businessCategories"; +import { businessCategories } from "../../config/data/businessCategories"; import { useOnboardingForm } from "../../hooks/useOnboardingForm"; import Tooltip from "../common/Tooltip/Tooltip"; @@ -172,7 +172,7 @@ export default function OnboardingForm() {
- + Business Category
- + Areas of Interest diff --git a/src/components/onboarding/database/AirbyteConfig.tsx b/src/components/onboarding/database/AirbyteConfig.tsx index 686202a..5194bbe 100644 --- a/src/components/onboarding/database/AirbyteConfig.tsx +++ b/src/components/onboarding/database/AirbyteConfig.tsx @@ -30,7 +30,7 @@ export default function AirbyteConfig({

Optional Airbyte Configuration

@@ -73,7 +73,7 @@ export default function AirbyteConfig({ }`} /> diff --git a/src/components/onboarding/database/AirbyteSyncButton.tsx b/src/components/onboarding/database/AirbyteSyncButton.tsx index a4740bc..aee067c 100644 --- a/src/components/onboarding/database/AirbyteSyncButton.tsx +++ b/src/components/onboarding/database/AirbyteSyncButton.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { RefreshCw, ChevronDown } from 'lucide-react'; -import { useSyncStatus } from '../../../lib/airbyte/useSyncStatus'; +import { useAirbyteSyncStatus } from '../../../hooks/useAirbyteSyncStatus'; import StatusIndicator from '../../common/ProgressLoader/StatusIndicator'; interface AirbyteSyncButtonProps { @@ -15,7 +15,7 @@ export default function AirbyteSyncButton({ token, connectionId }: AirbyteSyncBu const [isTriggered, setIsTriggered] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const [selectedJobType, setSelectedJobType] = useState('sync'); - const syncStatus = useSyncStatus( + const syncStatus = useAirbyteSyncStatus( isTriggered ? token : undefined, isTriggered ? connectionId : undefined, selectedJobType, diff --git a/src/components/onboarding/database/GroqConnection.tsx b/src/components/onboarding/database/GroqConnection.tsx index ff08ae9..e9a2d69 100644 --- a/src/components/onboarding/database/GroqConnection.tsx +++ b/src/components/onboarding/database/GroqConnection.tsx @@ -3,7 +3,7 @@ import { Brain, Loader2 } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import StatusIndicator from "../../common/ProgressLoader/StatusIndicator"; import Tooltip from "../../common/Tooltip/Tooltip"; -import { useGroqConnection } from "../../../lib/groq/useGroqConnection"; +import { useGroqConnection } from "../../../hooks/useGroqConnection"; interface GroqConnectionProps { token?: string; @@ -32,7 +32,7 @@ export default function GroqConnection({ return (
- + GROQ Configuration

{welcomeScreenData.welcomeSectionVersionBottom} diff --git a/src/data/businessCategories.ts b/src/config/data/businessCategories.ts similarity index 100% rename from src/data/businessCategories.ts rename to src/config/data/businessCategories.ts diff --git a/src/config/services/index.ts b/src/config/services/index.ts new file mode 100644 index 0000000..050d5da --- /dev/null +++ b/src/config/services/index.ts @@ -0,0 +1,20 @@ +export const AIRBYTE_SUPABASE_PROXY_URL = "https://lejxudxaxuqfkhtgddoe.supabase.co/functions/v1/apiProxy/airbyte"; +export const AIRBYTE_LOCAL_PROXY_URL = "/api/airbyte"; +export const AIRBYTE_API_BASE_URL = "https://api.airbyte.com/v1"; +export const GROQ_SUPABASE_PROXY_URL = "https://lejxudxaxuqfkhtgddoe.supabase.co/functions/v1/apiProxy/groq"; +export const GROQ_LOCAL_PROXY_URL = "/api/groq"; +export const GROQ_API_BASE_URL = "https://api.groq.com/v1"; + +export const getAirbyteApiUrl = () => { + if (import.meta.env.DEV) { + return AIRBYTE_LOCAL_PROXY_URL; + } + return AIRBYTE_SUPABASE_PROXY_URL; + }; + + export const getGroqApiUrl = () => { + if (import.meta.env.DEV) { + return GROQ_LOCAL_PROXY_URL; + } + return GROQ_SUPABASE_PROXY_URL; + }; diff --git a/src/lib/airbyte/useSyncStatus.ts b/src/hooks/useAirbyteSyncStatus.ts similarity index 91% rename from src/lib/airbyte/useSyncStatus.ts rename to src/hooks/useAirbyteSyncStatus.ts index b2c0ae7..ec86fc0 100644 --- a/src/lib/airbyte/useSyncStatus.ts +++ b/src/hooks/useAirbyteSyncStatus.ts @@ -1,8 +1,8 @@ import { useState, useEffect } from "react"; -import { checkConnectionStatus, triggerJob } from "./service"; -import type { StatusState } from "../../types/status"; +import { checkConnectionStatus, triggerJob } from "../lib/airbyte/service"; +import type { StatusState } from "../types/status"; -export function useSyncStatus( +export function useAirbyteSyncStatus( token?: string, connectionId?: string, jobType: "sync" | "reset" = "sync", diff --git a/src/lib/motherduck/useConnectionStatus.ts b/src/hooks/useConnectionStatus.ts similarity index 90% rename from src/lib/motherduck/useConnectionStatus.ts rename to src/hooks/useConnectionStatus.ts index d992a7c..05f2db0 100644 --- a/src/lib/motherduck/useConnectionStatus.ts +++ b/src/hooks/useConnectionStatus.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { getConnection } from "./connection"; -import { useTokenStore } from "./tokenStore"; -import type { StatusState } from "../../types/status"; +import { getConnection } from "../lib/motherduck/connection"; +import { useTokenStore } from "../lib/motherduck/tokenStore"; +import type { StatusState } from "../types/status"; export function useConnectionStatus( inputToken: string | undefined diff --git a/src/lib/groq/useGroqConnection.ts b/src/hooks/useGroqConnection.ts similarity index 96% rename from src/lib/groq/useGroqConnection.ts rename to src/hooks/useGroqConnection.ts index 795fb71..ee3a9dd 100644 --- a/src/lib/groq/useGroqConnection.ts +++ b/src/hooks/useGroqConnection.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Groq } from "groq-sdk"; -import type { StatusState } from "../../types/status"; +import type { StatusState } from "../types/status"; export function useGroqConnection(token?: string): StatusState { const [status, setStatus] = useState({ diff --git a/src/hooks/useMotherDuck.ts b/src/hooks/useMotherDuckConnection.ts similarity index 100% rename from src/hooks/useMotherDuck.ts rename to src/hooks/useMotherDuckConnection.ts diff --git a/src/lib/airbyte/service.ts b/src/lib/airbyte/service.ts index e198346..1be237a 100644 --- a/src/lib/airbyte/service.ts +++ b/src/lib/airbyte/service.ts @@ -1,6 +1,12 @@ import type { AirbyteConfig, SyncJobResponse } from "./types"; +import { getAirbyteApiUrl } from "../../config/services"; -const API_BASE_URL = "/api/airbyte"; +const noCacheHeaders = { + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + 'X-Request-Timestamp': Date.now().toString(), +}; export async function checkConnectionStatus( config: AirbyteConfig @@ -8,11 +14,12 @@ export async function checkConnectionStatus( try { if (!config.bearerToken) { throw new Error("Bearer token is required"); - } - - const response = await fetch(`${API_BASE_URL}/sources`, { + } + const url = getAirbyteApiUrl() + const response = await fetch(`${url}/sources?_=${Date.now()}`, { method: "GET", headers: { + ...noCacheHeaders, authorization: `Bearer ${config.bearerToken}`, accept: "application/json", }, @@ -22,8 +29,7 @@ export async function checkConnectionStatus( throw new Error(`Connection check failed: ${response.statusText}`); } - const data = await response.json(); - console.log(data); + await response.json(); return response.ok; } catch (error) { console.error("Connection check error:", error); @@ -40,9 +46,11 @@ export async function triggerJob( } try { - const response = await fetch(`${API_BASE_URL}/jobs`, { + const url = getAirbyteApiUrl() + const response = await fetch(`${url}/jobs?_=${Date.now()}`, { method: "POST", headers: { + ...noCacheHeaders, authorization: `Bearer ${config.bearerToken}`, accept: "application/json", "content-type": "application/json", diff --git a/src/lib/groq/models.ts b/src/lib/groq/models.ts index af4bc7e..1b10472 100644 --- a/src/lib/groq/models.ts +++ b/src/lib/groq/models.ts @@ -1,3 +1,4 @@ +import { getGroqApiUrl } from '../../config/services'; import type { GroqModel } from './types'; export const GROQ_MODELS: GroqModel[] = [ @@ -5,32 +6,36 @@ export const GROQ_MODELS: GroqModel[] = [ id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', description: 'Balanced performance for business analysis', - maxTokens: 32768 - } + maxTokens: 32768, + }, ]; export async function fetchAvailableModels(token: string): Promise { try { - const response = await fetch('https://api.groq.com/v1/models', { + const url = getGroqApiUrl() + const response = await fetch(`${url}/models`, { headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, }); if (!response.ok) { - throw new Error('Failed to fetch GROQ models'); + const errorData = await response.json().catch(() => null); + throw new Error( + errorData?.message || `Failed to fetch GROQ models: ${response.statusText}` + ); } const data = await response.json(); - return data.data.map((model: any) => ({ id: model.id, - name: model.id.split('-').map((word: string) => - word.charAt(0).toUpperCase() + word.slice(1) - ).join(' '), - description: 'AI model for analysis', - maxTokens: model.context_window || 8192 + name: model.id + .split('-') + .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '), + description: model.description || 'AI model for analysis', + maxTokens: model.context_window || 8192, })); } catch (error) { console.error('Failed to fetch GROQ models:', error); diff --git a/src/lib/motherduck/queries/analytics.ts b/src/lib/motherduck/queries/analytics.ts index 85954ba..fe2cdac 100644 --- a/src/lib/motherduck/queries/analytics.ts +++ b/src/lib/motherduck/queries/analytics.ts @@ -6,6 +6,7 @@ import { fetchPositiveInsights } from './positiveInsights'; import { fetchTextAnalysis } from './textAnalysis'; import type { ProcessedAnalytics } from '../types'; import type { DataLimit } from '../../../components/onboarding/DataSelectionStep'; +import { fetchSentimentInsights } from './sentimentInsights'; export async function fetchAnalytics( database: string, @@ -35,13 +36,15 @@ export async function fetchAnalytics( aspectAnalysis, negativeInsights, positiveInsights, - textAnalysis + textAnalysis, + sentimentData ] = await Promise.all([ connection.evaluateQuery(basicStatsQuery), fetchAspectAnalysis(database, tableName, limit), fetchNegativeInsights(database, tableName, limit), fetchPositiveInsights(database, tableName, limit), - fetchTextAnalysis(database, tableName, limit) + fetchTextAnalysis(database, tableName, limit), + fetchSentimentInsights(database, tableName, limit), ]); const stats = basicStats.data.toRows()[0]; @@ -56,7 +59,8 @@ export async function fetchAnalytics( aspectAnalysis, negativeInsights, positiveInsights, - textAnalysis + textAnalysis, + sentimentInsights: sentimentData, }; } catch (error) { console.error('Analytics query execution failed:', error); diff --git a/src/lib/motherduck/queries/keyPhrases.ts b/src/lib/motherduck/queries/keyPhrases.ts deleted file mode 100644 index d34405c..0000000 --- a/src/lib/motherduck/queries/keyPhrases.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { getConnection } from '../connection'; -import { getTableReference } from './utils'; -import type { KeyPhrase } from '../../../types/analytics'; -import type { Dataset } from '../types'; - -export async function fetchKeyPhrases(dataset: Dataset, limit: number | 'All'): Promise { - const connection = await getConnection(); - const limitClause = limit === 'All' ? '' : `LIMIT ${limit}`; - const tableRef = getTableReference(dataset); - - const query = ` - WITH review_stats AS ( - SELECT - CASE - WHEN stars >= 4 THEN 'excellent service' - WHEN stars = 3 THEN 'average experience' - ELSE 'poor quality' - END as phrase, - COUNT(*) as occurrences, - AVG(CASE - WHEN stars >= 4 THEN 1 - WHEN stars <= 2 THEN -1 - ELSE 0 - END) as sentiment - FROM ${tableRef} - GROUP BY - CASE - WHEN stars >= 4 THEN 'excellent service' - WHEN stars = 3 THEN 'average experience' - ELSE 'poor quality' - END - HAVING COUNT(*) >= 5 - ORDER BY occurrences DESC - ${limitClause} - ) - SELECT * FROM review_stats - `; - - const result = await connection.evaluateQuery(query); - return result.data.toRows().map(row => ({ - text: String(row.phrase), - occurrences: Number(row.occurrences), - sentiment: Number(row.sentiment) - })); -} diff --git a/src/lib/motherduck/queries/sentimentInsights.ts b/src/lib/motherduck/queries/sentimentInsights.ts new file mode 100644 index 0000000..240e921 --- /dev/null +++ b/src/lib/motherduck/queries/sentimentInsights.ts @@ -0,0 +1,79 @@ +import { getConnection } from '../connection'; +import { getTableRef } from './utils'; +import type { DataLimit } from '../../../components/onboarding/DataSelectionStep'; +import type { SentimentData } from '../../../components/dashboard/Analytics/SentimentHeatmap'; + + +export async function fetchSentimentInsights( + database: string, + tableName: string, + limit: DataLimit + ): Promise { + const connection = await getConnection(); + const tableRef = getTableRef(database, tableName); + const limitClause = limit === 'All' ? '' : `LIMIT ${limit}`; + + try { + const query = ` + WITH sample_data AS ( + SELECT review_text, stars + FROM ${tableRef} + WHERE review_text IS NOT NULL + AND stars IS NOT NULL + ORDER BY RANDOM() + ${limitClause} + ), + words AS ( + SELECT + word.value as word, + stars + FROM sample_data, + UNNEST(string_split( + regexp_replace(lower(review_text), '[^a-z\s]', ' ', 'g'), + ' ' + )) as word(value) + WHERE strlen(trim(word.value)) > 3 + ), + word_sentiments AS ( + SELECT + word, + COUNT(*) as frequency, + AVG(CASE + WHEN stars >= 4 THEN 1 + WHEN stars = 3 THEN 0 + ELSE -1 + END) as sentiment_score + FROM words + WHERE word NOT IN ( + 'this', 'that', 'with', 'from', 'what', 'where', 'when', + 'have', 'here', 'there', 'they', 'their', 'were', 'would', + 'could', 'should', 'about', 'which', 'thing', 'some', 'these' + ) + GROUP BY word + HAVING COUNT(*) >= 5 -- Reduced minimum frequency for testing + ) + SELECT + word as name, + frequency as size, + ROUND(CAST( + (sentiment_score + 1) / 2 as DOUBLE + ), 2) as sentiment + FROM word_sentiments + WHERE frequency > 0 + ORDER BY frequency DESC + LIMIT 30 + `; + + const result = await connection.evaluateQuery(query); + const rows = result.data.toRows(); + + return rows.map((row) => ({ + name: String(row.name), + size: Number(row.size), + sentiment: Number(row.sentiment) + })); + } catch (error) { + console.error('Sentiment analysis error:', error); + return []; + } + } \ No newline at end of file diff --git a/src/lib/motherduck/types.ts b/src/lib/motherduck/types.ts index d8d5e96..b51f350 100644 --- a/src/lib/motherduck/types.ts +++ b/src/lib/motherduck/types.ts @@ -1,3 +1,4 @@ +import { SentimentData } from "../../components/dashboard/Analytics/SentimentHeatmap"; import { AspectAnalysis, PositiveInsight, @@ -22,6 +23,7 @@ export interface ProcessedAnalytics { negativeInsights: NegativeInsight[]; positiveInsights: PositiveInsight[]; textAnalysis: TextAnalysis; + sentimentInsights?: SentimentData[]; } export type { AspectAnalysis }; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..944cd68 --- /dev/null +++ b/vercel.json @@ -0,0 +1,17 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cross-Origin-Opener-Policy", + "value": "same-origin" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-corp" + } + ] + } + ] +} diff --git a/vite.config.ts b/vite.config.ts index a39c908..6c2cd53 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,39 +1,96 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; -import { fixRequestBody } from 'http-proxy-middleware'; +import { defineConfig, loadEnv } from 'vite'; +import react from "@vitejs/plugin-react"; +import path from "path"; +import { fixRequestBody } from "http-proxy-middleware"; +import { + AIRBYTE_LOCAL_PROXY_URL as AB_PROX, + AIRBYTE_API_BASE_URL as AB_BASE, + GROQ_LOCAL_PROXY_URL as GROQ_PROX, + GROQ_API_BASE_URL as GROQ_BASE, +} from "./src/config/services/index"; -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - host: true, - headers: { - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, - proxy: { - '/api/airbyte': { - target: 'https://api.airbyte.com/v1', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/airbyte/, ''), - configure: (proxy, _options) => { - proxy.on('proxyReq', fixRequestBody); +export default defineConfig(({ mode }) => { + loadEnv(mode, process.cwd(), ""); + const AIRBYTE_API_PROXY_URL = AB_PROX; + const AIRBYTE_API_BASE_URL = AB_BASE; + const GROQ_API_PROXY_URL = GROQ_PROX; + const GROQ_API_BASE_URL = GROQ_BASE; + return { + plugins: [react()], + server: { + port: 5173, + host: true, + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + "Cache-Control": + "no-store, no-cache, must-revalidate, proxy-revalidate", + Pragma: "no-cache", + Expires: "0", + }, + proxy: { + [AIRBYTE_API_PROXY_URL]: { + target: AIRBYTE_API_BASE_URL, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/airbyte/, ""), + configure: (proxy, _options) => { + proxy.on("proxyReq", (proxyReq, req) => { + proxyReq.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate" + ); + proxyReq.setHeader("Pragma", "no-cache"); + proxyReq.setHeader("Expires", "0"); + fixRequestBody(proxyReq, req); + }); + + proxy.on("proxyRes", (proxyRes) => { + proxyRes.headers["cache-control"] = + "no-store, no-cache, must-revalidate, proxy-revalidate"; + proxyRes.headers["pragma"] = "no-cache"; + proxyRes.headers["expires"] = "0"; + }); + }, + }, + [GROQ_API_PROXY_URL]: { + target: GROQ_API_BASE_URL, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/groq/, ""), + configure: (proxy, _options) => { + proxy.on("proxyReq", (proxyReq, req) => { + proxyReq.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate" + ); + proxyReq.setHeader("Pragma", "no-cache"); + proxyReq.setHeader("Expires", "0"); + fixRequestBody(proxyReq, req); + }); + + proxy.on("proxyRes", (proxyRes) => { + proxyRes.headers["cache-control"] = + "no-store, no-cache, must-revalidate, proxy-revalidate"; + proxyRes.headers["pragma"] = "no-cache"; + proxyRes.headers["expires"] = "0"; + }); + }, }, }, - '/api/groq': { - target: 'http://localhost:3000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/groq/, ''), - configure: (proxy, _options) => { - proxy.on('proxyReq', fixRequestBody); + }, + build: { + outDir: "dist", + rollupOptions: { + output: { + entryFileNames: `assets/[name].[hash].js`, + chunkFileNames: `assets/[name].[hash].js`, + assetFileNames: `assets/[name].[hash].[ext]`, }, - } + }, }, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, }, - }, + }; });