├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── api └── vizchat.ts ├── index.html ├── package.json ├── postcss.config.js ├── public ├── datasets │ ├── cars.json │ └── students.json └── kanaries.ico ├── src ├── App.css ├── App.tsx ├── components │ ├── datasetCreation │ │ ├── dataTable.tsx │ │ ├── index.tsx │ │ └── pagination.tsx │ ├── dropdownContext.tsx │ ├── modal.tsx │ ├── monaco │ │ └── index.tsx │ ├── notify │ │ ├── index.tsx │ │ └── useNotification.tsx │ ├── react-vega.tsx │ ├── selectMenu.tsx │ ├── spinner.tsx │ ├── upgradeGuide │ │ ├── auth.ts │ │ └── index.tsx │ ├── vizChat.tsx │ └── welcomePrompt.tsx ├── index.css ├── interface.ts ├── main.tsx ├── services │ └── llm.ts ├── theme.ts ├── utils │ ├── index.ts │ ├── inferType.ts │ └── messageData.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | .vercel 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Elastic License 2.0 (ELv2) 2 | Elastic License 3 | Acceptance 4 | By using the software, you agree to all of the terms and conditions below. 5 | 6 | Copyright License 7 | The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below. 8 | 9 | Limitations 10 | You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software. 11 | 12 | You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. 13 | 14 | You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. 15 | 16 | Patents 17 | The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. 18 | 19 | Notices 20 | You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. 21 | 22 | If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. 23 | 24 | No Other Rights 25 | These terms do not imply any licenses other than those expressly granted in these terms. 26 | 27 | Termination 28 | If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. 29 | 30 | No Liability 31 | As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. 32 | 33 | Definitions 34 | The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it. 35 | 36 | you refers to the individual or entity agreeing to these terms. 37 | 38 | your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. 39 | 40 | your licenses are all the licenses granted to you for the software under these terms. 41 | 42 | use means anything you do with the software requiring one of your licenses. 43 | 44 | trademark means trademarks, service marks, and similar rights. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VizGPT: AI Data Visualization, Make contextual data visualization with Chat Interface 2 | 3 | https://github.com/ObservedObserver/viz-gpt/assets/22167673/a09032d3-f3c8-4cdf-ac14-89df8754fd9f 4 | 5 | 6 | Use GPT to generate visualization from datasets with natural language. You can edit the visualization in the context step by step to make it more precise without retyping the complex query. VizGPT turns your text queries and chat into data visualization or charts. 7 | 8 | You can try it at [Playground](https://vizgpt.ai/) 9 | or vizGPT on Kanaries [kanaries-vizgpt](https://kanaries.net/home/products) 10 | 11 | #### Why VizGPT 12 | 13 | There exist lots of great visualization products in the world, such as Tableau, pygwalker. The traditional drag-and-drop visualization tool is hard to use for people unfamiliar with configs and viz/data transformations. For example, making a tableau heatmap requires bin transformations to both axes and then dragging the measure to color. It is hard for people unfamiliar with data visualization to make a heatmap. 14 | 15 | Some text2viz tools accept natural language to generate the visualization. However, they are not flexible enough to allow users to edit the visualization. For example, if the user wants to change the color of the heatmap, they have to retype the whole sentence. 16 | 17 | With VizGPT, you can build visualizations step by step with a chat interface. You can edit/adjust visualizations in the context. It allows you to explore the data first without figuring out how to build complex visualization initially, especially when unfamiliar with the data. 18 | 19 | Besides, VizGPT focus on text based visual exploration. It allows users to discover new insights from visualization and ask new questions based on the insights they just find. 20 | 21 | ## Features & Roadmap 22 | + [x] Natural language to data visualization [vega-lite](https://github.com/vega/vega-lite) 23 | + [x] Use chat context to edit your visualization. Allow users to change the chart if it is not what they expected 24 | + [x] Explore the data step by step by chatting with visualizations. 25 | + [x] Upload your own dataset (CSV) to make visulizations. 26 | + [ ] Save the visualizations and chat history. 27 | + [ ] Allow user to use visualization editor (like [graphic-walker](https://github.com/Kanaries/graphic-walker) or [vega-editor](https://github.com/vega/editor)) to edit the visualization and show the edit to GPT to make better visualization as the user prefers. 28 | 29 | > vizGPT is now good at drawing data visualizations, not data transformations/preparation/computation. You can use other tools like Kanaries/RATH to prepare the data first and then use vizGPT to draw the visualization. 30 | 31 | ## vizGPT + RAG 32 | I also build a RAG version of vizGPT, which can be accessed at [vizgpt.ai](https://vizgpt.ai), it contains RAG special for vega/vega-lite and SQL. 33 | 34 | ## Chat to Viz Example 35 | vizapt-1 36 | 37 | vizapt-2 38 | 39 | vizapt-3 40 | 41 | ![Xnapper-2023-05-10-00 28 07](https://github.com/ObservedObserver/viz-gpt/assets/22167673/9ffb763a-d18d-4867-a974-5ab02131ce1f) 42 | 43 | ![Xnapper-2023-05-10-01 05 15](https://github.com/ObservedObserver/viz-gpt/assets/22167673/cd2d45c9-f0d4-431c-8ced-5c1228ad24a7) 44 | 45 | 46 | 47 | ### Add custom CSV file 48 | 49 | Click `upload CSV button to add your own data. You can view or edit your data's metas at data view. The metas are inferred automatically by default. You can edit it anytime you want to make the visualization more precise. 50 | 51 | ![data view](https://github.com/ObservedObserver/viz-gpt/assets/22167673/a490e364-bcd1-418f-80eb-62e47faf4330) 52 | 53 | 54 | 55 | ## Local Development 56 | 57 | #### step 1 58 | Create a `.env` file at the root of the project with the following contents: 59 | 60 | ``` 61 | BASE_URL= 62 | DEPLOYMENT_NAME= 63 | AZURE_OPENAI_KEY= 64 | ``` 65 | 66 | #### step 2 67 | 68 | Install dependencies: 69 | 70 | ```bash 71 | yarn install 72 | ``` 73 | 74 | #### step 3 75 | 76 | Then run `vercel dev` or `npm run dev` to start the server at port 3000. 77 | -------------------------------------------------------------------------------- /api/vizchat.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { VercelRequest, VercelResponse } from "@vercel/node"; 3 | import { IMessage } from "../src/services/llm"; 4 | import fetch from "node-fetch"; 5 | import { IField } from "../src/interface"; 6 | 7 | interface RequestBody { 8 | messages: IMessage[]; 9 | metas: IField[]; 10 | } 11 | export interface IResponseData { 12 | id: string; 13 | object: string; 14 | model: string; 15 | usage: { 16 | prompt_tokens: number; 17 | completion_tokens: number; 18 | total_tokens: number; 19 | }; 20 | choices: { message: { role: string; content: string } }[]; 21 | } 22 | 23 | const TEMPERATURE = 0.05; 24 | 25 | export default async function (req: VercelRequest, res: VercelResponse) { 26 | const { messages = [], metas = [] } = req.body as RequestBody; 27 | const systemMessage: IMessage = { 28 | role: "system", 29 | content: `You are a great assistant at vega-lite visualization creation. No matter what the user ask, you should always response with a valid vega-lite specification in JSON. 30 | 31 | You should create the vega-lite specification based on user's query. 32 | 33 | Besides, Here are some requirements: 34 | 1. Do not contain the key called 'data' in vega-lite specification. 35 | 2. If the user ask many times, you should generate the specification based on the previous context. 36 | 3. You should consider to aggregate the field if it is quantitative and the chart has a mark type of react, bar, line, area or arc. 37 | 4. Consider to use bin for field if it is a chart like heatmap or histogram. 38 | 5. The available fields in the dataset and their types are: 39 | ${metas 40 | .map((field) => `${field.name} (${field.semanticType})`) 41 | .join(", ")} 42 | `, 43 | }; 44 | // If the field is aggregated or transformed, the field title in spec should contains both the aggregate/transform info and the column title. For example, the title of field sales aggregated by mean should be "Mean(Sales)", 45 | if (messages.length === 0 || metas.length === 0) { 46 | res.status(400).json({ 47 | success: false, 48 | message: `[vizchat error] messages or metas is empty.`, 49 | }); 50 | return; 51 | } 52 | if (messages[messages.length - 1].role === "user") { 53 | messages[ 54 | messages.length - 1 55 | ].content = `Translate text delimited by triple backticks into vega-lite specification in JSON string. 56 | \`\`\` 57 | ${messages[messages.length - 1].content} 58 | \`\`\` 59 | `; 60 | // If there is no valid vega-lite specification or the instruction is not clear, you can recommend a chart from the given dataset and print in vega-lite JSON string. 61 | } 62 | try { 63 | const data = await getCompletion([systemMessage, ...messages]); 64 | res.status(200).json({ 65 | success: true, 66 | data: data, 67 | }); 68 | } catch (error) { 69 | res.status(500).json({ 70 | success: false, 71 | message: `[vizchat error] failed task.`, 72 | }); 73 | } 74 | return; 75 | } 76 | 77 | async function getCompletion(messages): Promise { 78 | if (preferAzureOpenAI()) { 79 | return getAzureOpenAICompletion(messages); 80 | } 81 | return getOpenAICompletion(messages) 82 | } 83 | 84 | function preferAzureOpenAI(): boolean { 85 | return !!(process.env.BASE_URL && process.env.DEPLOYMENT_NAME && process.env.AZURE_OPENAI_KEY); 86 | } 87 | 88 | async function getAzureOpenAICompletion(messages): Promise { 89 | const url = `${process.env.BASE_URL}/openai/deployments/${process.env.DEPLOYMENT_NAME}/chat/completions?api-version=2023-03-15-preview`; 90 | const response = await fetch(url, { 91 | method: "POST", 92 | // @ts-ignore 93 | headers: { 94 | "Content-Type": "application/json", 95 | // @ts-ignore 96 | "api-key": process.env.AZURE_OPENAI_KEY, 97 | }, 98 | body: JSON.stringify({ 99 | messages, 100 | temperature: TEMPERATURE, 101 | }), 102 | }); 103 | const data = (await response.json()) as IResponseData; 104 | return data; 105 | } 106 | 107 | 108 | async function getOpenAICompletion(messages): Promise { 109 | const url = "https://api.openai.com/v1/chat/completions"; 110 | const response = await fetch(url, { 111 | method: "POST", 112 | headers: { 113 | "Content-Type": "application/json", 114 | "Authorization": `Bearer ${process.env.OPENAI_KEY}` 115 | }, 116 | body: JSON.stringify({ 117 | "model": "gpt-3.5-turbo", 118 | messages: messages, 119 | temperature: TEMPERATURE, 120 | n: 1, 121 | }), 122 | }); 123 | 124 | const data = await response.json(); 125 | return data as IResponseData; 126 | } 127 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VizGPT 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viz-gpt", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "dev:vercel": "vercel dev" 12 | }, 13 | "prettier": { 14 | "tabWidth": 4, 15 | "printWidth": 120 16 | }, 17 | "dependencies": { 18 | "@headlessui/react": "^1.7.14", 19 | "@heroicons/react": "^2.0.17", 20 | "@kanaries/web-data-loader": "^0.1.7", 21 | "@monaco-editor/react": "^4.5.1", 22 | "@tailwindcss/forms": "^0.5.3", 23 | "@vercel/analytics": "^1.0.1", 24 | "@vercel/node": "^2.14.1", 25 | "immer": "^10.0.1", 26 | "monaco-editor": "^0.38.0", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "styled-components": "^6.0.0-rc.1", 30 | "vega": "^5.25.0", 31 | "vega-embed": "^6.22.1", 32 | "vega-lite": "^5.9.0" 33 | }, 34 | "devDependencies": { 35 | "@types/react": "^18.0.28", 36 | "@types/react-dom": "^18.0.11", 37 | "@types/styled-components": "^5.1.26", 38 | "@typescript-eslint/eslint-plugin": "^5.57.1", 39 | "@typescript-eslint/parser": "^5.57.1", 40 | "@vitejs/plugin-react": "^4.0.0", 41 | "autoprefixer": "^10.4.14", 42 | "eslint": "^8.38.0", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-react-refresh": "^0.3.4", 45 | "node-fetch": "^3.3.1", 46 | "postcss": "^8.4.23", 47 | "tailwindcss": "^3.3.2", 48 | "typescript": "^5.0.2", 49 | "vite": "^4.3.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/kanaries.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ObservedObserver/viz-gpt/b694660ab4dcd1ba91710f879d9ef57eb760f0ea/public/kanaries.ico -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ObservedObserver/viz-gpt/b694660ab4dcd1ba91710f879d9ef57eb760f0ea/src/App.css -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | import { useCallback, useEffect, useState } from "react"; 3 | import { track } from '@vercel/analytics'; 4 | import { IMessage, chatCompletation } from "./services/llm"; 5 | import { matchQuote } from "./utils"; 6 | import { IDataset } from "./interface"; 7 | import VizChat from "./components/vizChat"; 8 | import SelectMenu from "./components/selectMenu"; 9 | import { PaperAirplaneIcon, TrashIcon } from "@heroicons/react/20/solid"; 10 | import Spinner from "./components/spinner"; 11 | import DatasetCreation from "./components/datasetCreation"; 12 | import DataTable from "./components/datasetCreation/dataTable"; 13 | import { produce } from "immer"; 14 | import { useNotification } from "./components/notify/useNotification"; 15 | import { WelcomePrompt } from "./components/welcomePrompt"; 16 | import { useMessageData } from "./utils/messageData"; 17 | 18 | type DSItem = 19 | | { 20 | key: string; 21 | name: string; 22 | url: string; 23 | type: "demo"; 24 | } 25 | | { 26 | key: string; 27 | name: string; 28 | dataset: IDataset; 29 | type: "custom"; 30 | }; 31 | 32 | const EXAMPLE_DATASETS: DSItem[] = [ 33 | { 34 | key: "cars", 35 | name: "Cars Dataset", 36 | url: "/datasets/cars.json", 37 | type: "demo", 38 | }, 39 | { 40 | key: "students", 41 | name: "Students Dataset", 42 | url: "/datasets/students.json", 43 | type: "demo", 44 | }, 45 | ]; 46 | 47 | const HomePage = function HomePage() { 48 | const [userQuery, setUserQuery] = useState(""); 49 | const [loading, setLoading] = useState(false); 50 | const [dataset, setDataset] = useState(null); 51 | const [dsList, setDsList] = useState(EXAMPLE_DATASETS); 52 | const [pivotKey, setPivotKey] = useState("viz"); 53 | const [datasetKey, setDatasetKey] = useState( 54 | EXAMPLE_DATASETS[0].key 55 | ); 56 | const [chat, setChat] = useState([]); 57 | const { notify } = useNotification(); 58 | 59 | useEffect(() => { 60 | const currentDatasetInfo = 61 | dsList.find((dataset) => dataset.key === datasetKey) ?? dsList[0]; 62 | if (currentDatasetInfo.type === "demo") { 63 | fetch(currentDatasetInfo.url) 64 | .then((res) => res.json()) 65 | .then((res) => { 66 | setDataset(res); 67 | }) 68 | .catch(() => { 69 | notify( 70 | { 71 | title: "Error", 72 | message: "Dataset not found", 73 | type: "error", 74 | }, 75 | 3000 76 | ); 77 | }); 78 | } else { 79 | setDataset(currentDatasetInfo.dataset); 80 | } 81 | }, [datasetKey, dsList, notify]); 82 | 83 | const startQuery = useCallback(() => { 84 | setLoading(true); 85 | const latestQuery: IMessage = { 86 | role: "user", 87 | content: userQuery, 88 | }; 89 | const fields = dataset?.fields ?? []; 90 | track('query', { query: userQuery, chatSize: chat.length, keys: fields.map(f => f.fid).join(',') }) 91 | chatCompletation([...chat, latestQuery], fields) 92 | .then((res) => { 93 | if (res.choices.length > 0) { 94 | const spec = matchQuote( 95 | res.choices[0].message.content, 96 | "{", 97 | "}" 98 | ); 99 | if (spec) { 100 | setChat([...chat, latestQuery, res.choices[0].message]); 101 | } else { 102 | setChat([...chat, latestQuery, { 103 | role: 'assistant', 104 | content: 'There is no relative visualization for your query. Please check the dataset and try again.', 105 | }]); 106 | // throw new Error( 107 | // "No visualization matches your instruction.\n" + 108 | // res.choices[0].message.content 109 | // ); 110 | } 111 | } 112 | }) 113 | .catch((err) => { 114 | notify( 115 | { 116 | title: "Error", 117 | message: err.message, 118 | type: "error", 119 | }, 120 | 3000 121 | ); 122 | }) 123 | .finally(() => { 124 | setLoading(false); 125 | setUserQuery(""); 126 | }); 127 | }, [userQuery, chat, dataset, notify]); 128 | 129 | const clearChat = useCallback(() => { 130 | setChat([]); 131 | }, []); 132 | 133 | const feedbackHandler = useCallback( 134 | (messages: IMessage[], mIndex: number, action: string) => { 135 | // todo: implement feedback handler 136 | track('feedback', { query: messages[0]?.content, ans: messages[1]?.content, action }) 137 | notify({ 138 | title: "Feedback", 139 | message: "Thanks for your feedback!", 140 | type: "success", 141 | }, 1000) 142 | }, 143 | [notify] 144 | ); 145 | 146 | const datasetCreateHandler = useCallback((ds: IDataset) => { 147 | const timeString = new Date().toISOString().replace(/:/g, "-"); 148 | const k = "custom-" + timeString; 149 | setDsList((l) => [ 150 | ...l, 151 | { 152 | name: "Custom Dataset" + l.length, 153 | key: k, 154 | dataset: ds, 155 | type: "custom", 156 | }, 157 | ]); 158 | setDatasetKey(k); 159 | }, []); 160 | 161 | useMessageData(datasetCreateHandler); 162 | 163 | return ( 164 |
165 |
166 |

167 | VizGPT 168 |

169 | 170 |
171 |

172 | Make contextual data visualization with Chat Interface from tabular datasets. 173 |

174 |
175 |
176 | { 181 | setDatasetKey(dsKey); 182 | }} 183 | /> 184 |
185 | 186 |
187 | 188 | 201 | 214 | 215 |
216 |
217 | {pivotKey === "viz" && ( 218 |
219 | {dataset && chat.length === 0 && ( 220 | { 223 | setUserQuery(p); 224 | }} 225 | /> 226 | )} 227 | {dataset && chat.length > 0 && ( 228 | { 232 | if (message.role === "user") { 233 | setChat((c) => { 234 | const newChat = [...c]; 235 | newChat.splice(mIndex, 2); 236 | return newChat; 237 | }); 238 | } else if (message.role === 'assistant') { 239 | setChat((c) => { 240 | const newChat = [...c]; 241 | newChat.splice(mIndex - 1, 2); 242 | return newChat; 243 | }); 244 | } 245 | }} 246 | onUserFeedback={feedbackHandler} 247 | /> 248 | )} 249 |
250 | 259 | setUserQuery(e.target.value)} 265 | onKeyDown={(e) => { 266 | if (e.key === "Enter" && loading === false && userQuery.length > 0) { 267 | startQuery(); 268 | } 269 | }} 270 | /> 271 | 283 |
284 |
285 | )} 286 | {pivotKey === "data" && ( 287 |
288 | {dataset && ( 289 | { 293 | const nextDataset = produce( 294 | dataset, 295 | (draft) => { 296 | draft.fields[fIndex] = { 297 | ...draft.fields[fIndex], 298 | ...meta, 299 | }; 300 | } 301 | ); 302 | setDataset(nextDataset); 303 | }} 304 | /> 305 | )} 306 |
307 | )} 308 |
309 | ); 310 | }; 311 | 312 | export default HomePage; 313 | -------------------------------------------------------------------------------- /src/components/datasetCreation/dataTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { IField, IRow } from "../../interface"; 4 | import Pagination from "./pagination"; 5 | import { ChevronUpDownIcon } from "@heroicons/react/24/outline"; 6 | import DropdownContext from "../dropdownContext" 7 | 8 | interface DataTableProps { 9 | size?: number; 10 | metas: IField[]; 11 | data: IRow[]; 12 | onMetaChange: (fid: string, fIndex: number, meta: Partial) => void; 13 | } 14 | const Container = styled.div` 15 | overflow-x: auto; 16 | max-height: 660px; 17 | overflow-y: auto; 18 | table { 19 | box-sizing: content-box; 20 | border-collapse: collapse; 21 | font-size: 12px; 22 | tbody { 23 | td { 24 | } 25 | td.number { 26 | text-align: right; 27 | } 28 | td.text { 29 | text-align: left; 30 | } 31 | } 32 | } 33 | `; 34 | const SEMANTIC_TYPE_LIST = ["nominal", "ordinal", "quantitative", "temporal"]; 35 | // function getCellType(field: IField): 'number' | 'text' { 36 | // return field.dataType === 'number' || field.dataType === 'integer' ? 'number' : 'text'; 37 | // } 38 | function getHeaderType(field: IField): "number" | "text" { 39 | return field.semanticType !== "quantitative" ? "text" : "number"; 40 | } 41 | 42 | function getHeaderClassNames(field: IField) { 43 | return field.semanticType !== "quantitative" ? "border-t-4 border-blue-400" : "border-t-4 border-purple-400"; 44 | } 45 | 46 | function getSemanticColors(field: IField): string { 47 | switch (field.semanticType) { 48 | case "nominal": 49 | return "border border-transparent bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-100 dark:border-sky-600"; 50 | case "ordinal": 51 | return "border border-transparent bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-100 dark:border-indigo-600"; 52 | case "quantitative": 53 | return "border border-transparent bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100 dark:border-purple-600"; 54 | case "temporal": 55 | return "border border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100 dark:border-yellow-600"; 56 | default: 57 | return "border border-transparent bg-gray-400"; 58 | } 59 | } 60 | 61 | const DataTable: React.FC = (props) => { 62 | const { size = 10, data, metas, onMetaChange } = props; 63 | const [pageIndex, setPageIndex] = useState(0); 64 | 65 | const semanticTypeList = useMemo<{ value: string; label: string }[]>(() => { 66 | return SEMANTIC_TYPE_LIST.map((st) => ({ 67 | value: st, 68 | label: st 69 | })); 70 | }, []); 71 | 72 | const from = pageIndex * size; 73 | const to = Math.min((pageIndex + 1) * size, data.length - 1); 74 | 75 | return ( 76 | 77 | { 82 | setPageIndex(Math.min(Math.ceil(data.length / size) - 1, pageIndex + 1)); 83 | }} 84 | onPrev={() => { 85 | setPageIndex(Math.max(0, pageIndex - 1)); 86 | }} 87 | /> 88 | 89 | 90 | 91 | {metas.map((field, fIndex) => ( 92 | 122 | ))} 123 | 124 | 125 | 126 | {data.slice(from, to + 1).map((row, index) => ( 127 | 128 | {metas.map((field) => ( 129 | 138 | ))} 139 | 140 | ))} 141 | 142 |
93 |
99 | {field.name || field.fid} 100 |
101 | { 104 | onMetaChange(field.fid, fIndex, { 105 | semanticType: value as IField["semanticType"], 106 | }); 107 | }} 108 | > 109 | 115 | {field.semanticType} 116 | 117 | 118 | 119 |
120 |
121 |
136 | {`${row[field.fid]}`} 137 |
143 |
144 | ); 145 | }; 146 | 147 | export default DataTable; 148 | -------------------------------------------------------------------------------- /src/components/datasetCreation/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from "react"; 2 | import { produce } from "immer"; 3 | import { IDataset, IRow } from "../../interface"; 4 | import { FileReader } from "@kanaries/web-data-loader"; 5 | import { inferDatasetMeta } from "../../utils/inferType"; 6 | import Modal from "../modal"; 7 | import DataTable from "./dataTable"; 8 | import UpgradeGuide from "../upgradeGuide"; 9 | import { hasAccess } from "../upgradeGuide/auth"; 10 | import { track } from "@vercel/analytics"; 11 | 12 | interface DatasetCreationProps { 13 | onDatasetCreated: (dataset: IDataset) => void; 14 | } 15 | 16 | export default function DatasetCreation(props: DatasetCreationProps) { 17 | const { onDatasetCreated } = props; 18 | const fileRef = useRef(null); 19 | const [modalOpen, setModalOpen] = useState(false); 20 | const [tmpDataset, setTmpDataset] = useState(null); 21 | const showTrigger = useRef<(show: boolean) => void>(null); 22 | 23 | const fileUpload = useCallback( 24 | (e: React.ChangeEvent) => { 25 | const files = e.target.files; 26 | if (files !== null) { 27 | const file = files[0]; 28 | FileReader.csvReader({ 29 | file, 30 | config: { type: "reservoirSampling", size: Infinity }, 31 | encoding: 'utf-8' 32 | }).then((data) => { 33 | const dataset = inferDatasetMeta(data as IRow[]); 34 | // onDatasetCreated(dataset); 35 | setModalOpen(true); 36 | setTmpDataset(dataset); 37 | }); 38 | } 39 | }, 40 | [] 41 | ); 42 | 43 | return ( 44 |
45 | 46 | 60 | 66 | { 69 | setModalOpen(false); 70 | }} 71 | > 72 | {tmpDataset && ( 73 | { 77 | const nextDataset = produce(tmpDataset, (draft) => { 78 | draft.fields[fIndex] = { 79 | ...draft.fields[fIndex], 80 | ...meta, 81 | }; 82 | }); 83 | setTmpDataset(nextDataset); 84 | }} 85 | /> 86 | )} 87 |
88 | 100 |
101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/datasetCreation/pagination.tsx: -------------------------------------------------------------------------------- 1 | interface PaginationProps { 2 | from: number; 3 | to: number; 4 | total: number; 5 | onPrev: () => void; 6 | onNext: () => void; 7 | } 8 | export default function Pagination(props: PaginationProps) { 9 | const { from, to, total, onNext, onPrev } = props; 10 | return ( 11 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/dropdownContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Menu, Transition } from "@headlessui/react"; 3 | 4 | function classNames(...classes: string[]) { 5 | return classes.filter(Boolean).join(" "); 6 | } 7 | 8 | export interface IDropdownContextOption { 9 | label: string; 10 | value: string; 11 | disabled?: boolean; 12 | } 13 | interface IDropdownContextProps { 14 | options?: IDropdownContextOption[]; 15 | disable?: boolean; 16 | onSelect?: (value: string, index: number) => void; 17 | children: React.ReactNode; 18 | } 19 | const DropdownContext: React.FC = (props) => { 20 | const { options = [], disable } = props; 21 | 22 | if (disable) { 23 | return {props.children}; 24 | } 25 | 26 | return ( 27 | 28 | {props.children} 29 | 30 | 39 | 40 |
41 | {options.map((option, index) => ( 42 | 43 | {(p) => ( 44 | { 50 | props.onSelect && !props.disable && props.onSelect(option.value, index); 51 | }} 52 | > 53 | {option.label} 54 | 55 | )} 56 | 57 | ))} 58 |
59 |
60 |
61 |
62 | ); 63 | }; 64 | 65 | export default DropdownContext; 66 | -------------------------------------------------------------------------------- /src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | XMarkIcon, 5 | } from "@heroicons/react/24/outline"; 6 | 7 | const Background = styled.div({ 8 | position: "fixed", 9 | left: 0, 10 | top: 0, 11 | width: "100vw", 12 | height: "100vh", 13 | backdropFilter: "blur(1px)", 14 | zIndex: 25535, 15 | }); 16 | 17 | const Container = styled.div` 18 | width: 98%; 19 | @media (min-width: 600px) { 20 | width: 80%; 21 | } 22 | @media (min-width: 1100px) { 23 | width: 880px; 24 | } 25 | max-height: 800px; 26 | overflow: auto; 27 | > div.container { 28 | padding: 0.5em 1em 1em 1em; 29 | } 30 | position: fixed; 31 | left: 50%; 32 | top: 50%; 33 | transform: translate(-50%, -50%); 34 | /* box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.19); */ 35 | border-radius: 4px; 36 | z-index: 999; 37 | `; 38 | interface ModalProps { 39 | onClose?: () => void; 40 | show?: boolean; 41 | title?: string; 42 | children?: React.ReactNode; 43 | } 44 | const Modal: React.FC = (props) => { 45 | const { onClose, title, show } = props; 46 | const prevMouseDownTimeRef = useRef(0); 47 | return ( 48 | (prevMouseDownTimeRef.current = Date.now())} 59 | onMouseOut={() => (prevMouseDownTimeRef.current = 0)} 60 | onMouseUp={() => { 61 | if (Date.now() - prevMouseDownTimeRef.current < 1000) { 62 | onClose?.(); 63 | } 64 | }} 65 | > 66 | e.stopPropagation()} 70 | > 71 |
72 | 82 |
83 |
84 | {title} 85 |
86 |
{props.children}
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default Modal; 93 | -------------------------------------------------------------------------------- /src/components/monaco/index.tsx: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | import MonacoEditor, { loader } from '@monaco-editor/react'; 3 | import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; 4 | import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; 5 | import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; 6 | import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; 7 | import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; 8 | 9 | 10 | if (!self.MonacoEnvironment) { 11 | self.MonacoEnvironment = { 12 | getWorker(_, label) { 13 | if (label === "json") { 14 | return new jsonWorker(); 15 | } 16 | if (label === "css" || label === "scss" || label === "less") { 17 | return new cssWorker(); 18 | } 19 | if (label === "html" || label === "handlebars" || label === "razor") { 20 | return new htmlWorker(); 21 | } 22 | if (label === "typescript" || label === "javascript") { 23 | return new tsWorker(); 24 | } 25 | return new editorWorker(); 26 | }, 27 | }; 28 | loader.config({ monaco }); 29 | loader.init(); 30 | } 31 | 32 | 33 | export default MonacoEditor; -------------------------------------------------------------------------------- /src/components/notify/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Fragment, 3 | useState, 4 | createContext, 5 | useCallback, 6 | ReactElement, 7 | } from "react"; 8 | import { Transition } from "@headlessui/react"; 9 | import { CheckCircleIcon } from "@heroicons/react/24/outline"; 10 | import { XMarkIcon } from "@heroicons/react/20/solid"; 11 | 12 | export interface INotification { 13 | title: string; 14 | message: string; 15 | type: "success" | "error" | "info" | "warning"; 16 | } 17 | 18 | export const NotificationContext = createContext<{ 19 | notify: (not: INotification, t: number) => void; 20 | }>(null!); 21 | 22 | function getNotificationIcon(type: INotification["type"]) { 23 | switch (type) { 24 | case "success": 25 | return ( 26 |